commit 8c932b13b0c7d90171f9800f2361a30a45f01ff4 Author: arch_agent Date: Sun Apr 19 15:39:23 2026 +0200 Initial commit - Mobile Wallpaper Service diff --git a/app.py b/app.py new file mode 100644 index 0000000..b23d03f --- /dev/null +++ b/app.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""Mobile Wallpaper Processor - Flask Web Service +Skaliert, Schneidet zu und entfernt Text aus Bildern für 1440x3120 Displays +""" + +import os +import cv2 +import numpy as np +import uuid +import time +from datetime import datetime, timedelta +from flask import Flask, render_template, request, send_file, jsonify, url_for +from werkzeug.utils import secure_filename +from PIL import Image, ImageEnhance +import easyocr +import threading + +# Konfiguration +TARGET_WIDTH = 1440 +TARGET_HEIGHT = 3120 +TARGET_ASPECT = TARGET_WIDTH / TARGET_HEIGHT +UPLOAD_FOLDER = 'static/uploads' +DOWNLOAD_FOLDER = 'static/downloads' +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'tiff'} +MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB + +app = Flask(__name__) +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +app.config['DOWNLOAD_FOLDER'] = DOWNLOAD_FOLDER +app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE + +# Globaler OCR Reader (wird einmal geladen) +ocr_reader = None +def get_ocr_reader(): + global ocr_reader + if ocr_reader is None: + print("Lade OCR Model...") + ocr_reader = easyocr.Reader(['de', 'en'], gpu=False) + return ocr_reader + +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +def cleanup_old_files(): + """Löscht Dateien älter als 1 Stunde""" + cutoff = datetime.now() - timedelta(hours=1) + for folder in [UPLOAD_FOLDER, DOWNLOAD_FOLDER]: + for filename in os.listdir(folder): + filepath = os.path.join(folder, filename) + try: + if os.path.isfile(filepath): + file_time = datetime.fromtimestamp(os.path.getmtime(filepath)) + if file_time < cutoff: + os.remove(filepath) + except Exception as e: + print(f"Cleanup error: {e}") + +def smart_crop(img_array, target_width, target_height): + """Intelligentes Zuschneiden mit Fokus auf Bildzentrum""" + img = Image.fromarray(cv2.cvtColor(img_array, cv2.COLOR_BGR2RGB)) + original_width, original_height = img.size + target_aspect = target_width / target_height + original_aspect = original_width / original_height + + if original_aspect > target_aspect: + # Bild ist breiter als Ziel: Horizontal zuschneiden + new_width = int(original_height * target_aspect) + left = (original_width - new_width) // 2 + img_cropped = img.crop((left, 0, left + new_width, original_height)) + else: + # Bild ist höher als Ziel: Vertikal zuschneiden + new_height = int(original_width / target_aspect) + top = (original_height - new_height) // 2 + img_cropped = img.crop((0, top, original_width, top + new_height)) + + # Auf Zielgröße skalieren + img_resized = img_cropped.resize((target_width, target_height), Image.Resampling.LANCZOS) + return cv2.cvtColor(np.array(img_resized), cv2.COLOR_RGB2BGR) + +def detect_text_regions(img): + """Erkennt Textbereiche im Bild mit OCR""" + reader = get_ocr_reader() + results = reader.readtext(img) + + mask = np.zeros(img.shape[:2], dtype=np.uint8) + detected_boxes = [] + + for (bbox, text, conf) in results: + if conf > 0.3: # Nur Text mit ausreichender Konfidenz + # bbox sind 4 Punkte: [[x1,y1], [x2,y2], [x3,y3], [x4,y4]] + pts = np.array(bbox, np.int32) + # Erweitere Box um Padding + x_coords = [p[0] for p in bbox] + y_coords = [p[1] for p in bbox] + x_min, x_max = max(0, min(x_coords) - 10), min(img.shape[1], max(x_coords) + 10) + y_min, y_max = max(0, min(y_coords) - 10), min(img.shape[0], max(y_coords) + 10) + + cv2.rectangle(mask, (x_min, y_min), (x_max, y_max), 255, -1) + detected_boxes.append({'text': text, 'confidence': conf, 'box': [x_min, y_min, x_max, y_max]}) + + return mask, detected_boxes + +def remove_text_inpaint(img, mask): + """Entfernt Text durch OpenCV Inpainting""" + if np.sum(mask) == 0: + return img + + # Telea Inpainting - gut für kleine strukturierte Bereiche wie Text + result = cv2.inpaint(img, mask, 3, cv2.INPAINT_TELEA) + return result + +def auto_enhance(img): + """Automatische Bildverbesserung""" + pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) + + # Kontrast leicht erhöhen + enhancer = ImageEnhance.Contrast(pil_img) + pil_img = enhancer.enhance(1.1) + + # Sättigung leicht erhöhen + enhancer = ImageEnhance.Color(pil_img) + pil_img = enhancer.enhance(1.1) + + # Schärfen + enhancer = ImageEnhance.Sharpness(pil_img) + pil_img = enhancer.enhance(1.2) + + return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) + +def process_image(input_path, remove_text=True): + """Hauptverarbeitungs-Pipeline""" + + # Bild laden + img = cv2.imread(input_path) + if img is None: + raise ValueError("Bild konnte nicht geladen werden") + + # Optional: Text entfernen + if remove_text: + mask, detected_text = detect_text_regions(img) + if len(detected_text) > 0: + img = remove_text_inpaint(img, mask) + + # Smart Crop auf Zielauflösung + img_processed = smart_crop(img, TARGET_WIDTH, TARGET_HEIGHT) + + # Auto-Enhance + img_processed = auto_enhance(img_processed) + + return img_processed + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/upload', methods=['POST']) +def upload_file(): + if 'file' not in request.files: + return jsonify({'error': 'Keine Datei hochgeladen'}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + if not allowed_file(file.filename): + return jsonify({'error': 'Dateiformat nicht erlaubt. Erlaubt: ' + str(ALLOWED_EXTENSIONS)}), 400 + + try: + # Cleanup alte Dateien + cleanup_old_files() + + # Eindeutigen Dateinamen generieren + filename = str(uuid.uuid4())[:8] + '_' + secure_filename(file.filename) + input_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(input_path) + + # Optionen + remove_text = request.form.get('remove_text', 'true').lower() == 'true' + + # Bild verarbeiten + processed = process_image(input_path, remove_text=remove_text) + + # Ausgabedatei + output_filename = 'processed_' + os.path.splitext(filename)[0] + '.jpg' + output_path = os.path.join(app.config['DOWNLOAD_FOLDER'], output_filename) + cv2.imwrite(output_path, processed, [cv2.IMWRITE_JPEG_QUALITY, 95]) + + # Original löschen + os.remove(input_path) + + return jsonify({ + 'success': True, + 'download_url': url_for('download_file', filename=output_filename), + 'preview_url': '/' + output_path, + 'message': f'Bild verarbeitet: {TARGET_WIDTH}x{TARGET_HEIGHT}px' + }) + + except Exception as e: + print(f"Error: {e}") + return jsonify({'error': str(e)}), 500 + +@app.route('/download/') +def download_file(filename): + return send_file(os.path.join(app.config['DOWNLOAD_FOLDER'], filename), + as_attachment=True, download_name='wallpaper_1440x3120.jpg') + +@app.route('/cleanup', methods=['POST']) +def manual_cleanup(): + cleanup_old_files() + return jsonify({'success': True, 'message': 'Aufgeräumt'}) + +if __name__ == '__main__': + print(f"Starting Mobile Wallpaper Service...") + print(f"Zielauflösung: {TARGET_WIDTH}x{TARGET_HEIGHT} ({TARGET_ASPECT:.3f})") + 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..bf31926 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +flask>=2.3.3 +opencv-python-headless>=4.9.0 +pillow>=10.2.0 +numpy>=1.26.0 +werkzeug>=2.3.7 +easyocr>=1.7.0 diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..0bba4df --- /dev/null +++ b/start.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Installation und Start Script für Mobile Wallpaper Processor + +echo "🚀 Mobile Wallpaper Processor Setup" +echo "====================================" + +# Prüfe Python +if ! command -v python3 &> /dev/null; then + echo "❌ Python3 ist nicht installiert" + exit 1 +fi + +cd "$(dirname "$0")" + +# Virtuelle Umgebung erstellen +if [ ! -d "venv" ]; then + echo "📦 Erstelle virtuelle Umgebung..." + python3 -m venv venv +fi + +# Aktivieren +source venv/bin/activate + +# Abhängigkeiten installieren +echo "📥 Installiere Abhängigkeiten..." +pip install --upgrade pip +pip install -r requirements.txt + +# Download OCR Model beim ersten Start +echo "🤖 Lade OCR Model herunter (einmalig)..." +python3 -c "import easyocr; easyocr.Reader(['de','en'], gpu=False)" 2>/dev/null || true + +# Ordner erstellen +mkdir -p static/uploads static/downloads + +echo "" +echo "✅ Installation abgeschlossen!" +echo "" +echo "Starte Server auf http://localhost:5000" +echo "" +python3 app.py diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..4cb9abe --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,421 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary: #6366f1; + --primary-dark: #4f46e5; + --bg-dark: #0f0f1a; + --bg-card: #1a1a2e; + --bg-hover: #252542; + --text: #e2e8f0; + --text-muted: #94a3b8; + --border: #2d2d4a; + --success: #22c55e; + --error: #ef4444; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--bg-dark); + color: var(--text); + min-height: 100vh; + line-height: 1.6; +} + +.container { + max-width: 900px; + margin: 0 auto; + padding: 40px 20px; +} + +/* Header */ +header { + text-align: center; + margin-bottom: 40px; +} + +h1 { + font-size: 2rem; + font-weight: 700; + background: linear-gradient(135deg, var(--primary), #a855f7); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 8px; +} + +.subtitle { + color: var(--text-muted); + font-size: 1rem; +} + +/* Upload Zone */ +.upload-zone { + background: var(--bg-card); + border: 2px dashed var(--border); + border-radius: 16px; + padding: 60px 40px; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; +} + +.upload-zone:hover, .upload-zone.dragover { + border-color: var(--primary); + background: var(--bg-hover); +} + +.upload-icon { + width: 64px; + height: 64px; + color: var(--primary); + margin-bottom: 16px; +} + +.upload-text { + font-size: 1.1rem; + font-weight: 500; + margin-bottom: 8px; +} + +.upload-hint { + color: var(--text-muted); + font-size: 0.875rem; +} + +/* Options */ +.options { + margin-top: 24px; + margin-bottom: 30px; +} + +.checkbox-label { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px 20px; + background: var(--bg-card); + border-radius: 12px; + cursor: pointer; + border: 1px solid var(--border); + transition: border-color 0.2s; +} + +.checkbox-label:hover { + border-color: var(--primary); +} + +.checkbox-label input { + display: none; +} + +.checkmark { + width: 22px; + height: 22px; + min-width: 22px; + border: 2px solid var(--border); + border-radius: 6px; + position: relative; + transition: all 0.2s; +} + +.checkbox-label input:checked + .checkmark { + background: var(--primary); + border-color: var(--primary); +} + +.checkbox-label input:checked + .checkmark::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + font-size: 14px; + font-weight: bold; +} + +.option-text { + display: flex; + flex-direction: column; +} + +.option-text strong { + font-weight: 600; + color: var(--text); +} + +.option-text small { + color: var(--text-muted); + font-size: 0.85rem; + margin-top: 2px; +} + +/* Preview */ +.preview { + margin-bottom: 30px; +} + +.preview h3 { + margin-bottom: 16px; + font-weight: 600; +} + +.preview-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +.preview-item { + text-align: center; +} + +.preview-item img { + width: 100%; + max-height: 200px; + object-fit: contain; + border-radius: 8px; + background: var(--bg-card); +} + +.preview-label { + display: block; + margin-top: 8px; + color: var(--text-muted); + font-size: 0.875rem; +} + +/* Phone Frame */ +.phone-frame { + position: relative; + width: 120px; + height: 260px; + margin: 0 auto; + background: #000; + border-radius: 24px; + padding: 8px; + box-shadow: 0 20px 50px rgba(0,0,0,0.5); +} + +.phone-screen { + width: 100%; + height: 100%; + overflow: hidden; + border-radius: 16px; + background: var(--bg-card); +} + +.phone-screen img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.phone-notch { + position: absolute; + top: 12px; + left: 50%; + transform: translateX(-50%); + width: 60px; + height: 18px; + background: #000; + border-radius: 10px; +} + +/* Progress */ +.progress { + background: var(--bg-card); + border-radius: 16px; + padding: 30px; + text-align: center; +} + +.progress-bar { + width: 100%; + height: 6px; + background: var(--border); + border-radius: 3px; + overflow: hidden; + margin-bottom: 16px; +} + +.progress-fill { + height: 100%; + width: 0%; + background: linear-gradient(90deg, var(--primary), #a855f7); + border-radius: 3px; + animation: progress 2s ease-in-out infinite; +} + +@keyframes progress { + 0% { width: 0%; opacity: 1; } + 50% { width: 100%; opacity: 1; } + 100% { width: 100%; opacity: 0; } +} + +.progress-text { + color: var(--text-muted); + margin-bottom: 20px; +} + +.processing-steps { + display: flex; + flex-direction: column; + gap: 8px; +} + +.step { + color: var(--text-muted); + font-size: 0.875rem; + padding: 6px 12px; + border-radius: 20px; + transition: all 0.3s; +} + +.step.active { + color: var(--primary); + background: rgba(99, 102, 241, 0.1); + font-weight: 500; +} + +.step::before { + content: '○ '; +} + +.step.active::before { + content: '● '; +} + +/* Result */ +.result { + text-align: center; +} + +.result-phone { + width: 200px; + height: 430px; + margin: 0 auto 30px; + background: #000; + border-radius: 36px; + padding: 12px; + box-shadow: 0 20px 80px rgba(0,0,0,0.6); +} + +.result-phone img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 24px; +} + +.result-actions { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; + margin-bottom: 24px; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 14px 28px; + border-radius: 12px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + border: none; + text-decoration: none; + transition: all 0.2s; +} + +.btn-primary { + background: var(--primary); + color: white; +} + +.btn-primary:hover { + background: var(--primary-dark); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(99, 102, 241, 0.4); +} + +.btn-secondary { + background: var(--bg-card); + color: var(--text); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + background: var(--bg-hover); + border-color: var(--primary); +} + +.result-info { + color: var(--text-muted); + font-size: 0.95rem; + line-height: 1.8; +} + +.result-info strong { + color: var(--text); +} + +/* Error */ +.error { + background: rgba(239, 68, 68, 0.1); + border: 1px solid var(--error); + color: #fca5a5; + padding: 16px; + border-radius: 12px; + margin-top: 20px; +} + +/* Footer */ +footer { + margin-top: 60px; + text-align: center; + color: var(--text-muted); + font-size: 0.85rem; +} + +/* Utilities */ +.hidden { + display: none !important; +} + +/* Mobile */ +@media (max-width: 600px) { + .container { + padding: 20px 16px; + } + + h1 { + font-size: 1.5rem; + } + + .upload-zone { + padding: 40px 20px; + } + + .preview-container { + grid-template-columns: 1fr; + } + + .phone-frame { + transform: scale(0.8); + } + + .result-actions { + flex-direction: column; + } + + .btn { + width: 100%; + justify-content: center; + } +} diff --git a/static/downloads/processed_188c02d8_frieren_and_fern_sousou_no_frieren_drawn_by_arkilide__40262d1834d8f6775383b2bce846f66e.jpg b/static/downloads/processed_188c02d8_frieren_and_fern_sousou_no_frieren_drawn_by_arkilide__40262d1834d8f6775383b2bce846f66e.jpg new file mode 100644 index 0000000..7dd1538 Binary files /dev/null and b/static/downloads/processed_188c02d8_frieren_and_fern_sousou_no_frieren_drawn_by_arkilide__40262d1834d8f6775383b2bce846f66e.jpg differ diff --git a/static/downloads/processed_1bb09331_9ed7f514eaddb260489459491bf5e889.jpg b/static/downloads/processed_1bb09331_9ed7f514eaddb260489459491bf5e889.jpg new file mode 100644 index 0000000..2b6d39e Binary files /dev/null and b/static/downloads/processed_1bb09331_9ed7f514eaddb260489459491bf5e889.jpg differ diff --git a/static/downloads/processed_1eb6d823_578b0a90a25a39801646f4e68ddeb4fb.jpg b/static/downloads/processed_1eb6d823_578b0a90a25a39801646f4e68ddeb4fb.jpg new file mode 100644 index 0000000..4b3bb1f Binary files /dev/null and b/static/downloads/processed_1eb6d823_578b0a90a25a39801646f4e68ddeb4fb.jpg differ diff --git a/static/downloads/processed_1fe2453e_79d137bc1d001e06837f5e6528b86498.jpg b/static/downloads/processed_1fe2453e_79d137bc1d001e06837f5e6528b86498.jpg new file mode 100644 index 0000000..04eaaef Binary files /dev/null and b/static/downloads/processed_1fe2453e_79d137bc1d001e06837f5e6528b86498.jpg differ diff --git a/static/downloads/processed_21a31a66_4bec1c1c88eda5e262e2d929f24bbb9c.jpg b/static/downloads/processed_21a31a66_4bec1c1c88eda5e262e2d929f24bbb9c.jpg new file mode 100644 index 0000000..02d87db Binary files /dev/null and b/static/downloads/processed_21a31a66_4bec1c1c88eda5e262e2d929f24bbb9c.jpg differ diff --git a/static/downloads/processed_63820e4d_d4a667ee5eb8856739aa017b168d26f1.jpg b/static/downloads/processed_63820e4d_d4a667ee5eb8856739aa017b168d26f1.jpg new file mode 100644 index 0000000..4ed8a67 Binary files /dev/null and b/static/downloads/processed_63820e4d_d4a667ee5eb8856739aa017b168d26f1.jpg differ diff --git a/static/downloads/processed_8a09c339_1fde37c18341f572246d68102a15c4cb.jpg b/static/downloads/processed_8a09c339_1fde37c18341f572246d68102a15c4cb.jpg new file mode 100644 index 0000000..8ed86d9 Binary files /dev/null and b/static/downloads/processed_8a09c339_1fde37c18341f572246d68102a15c4cb.jpg differ diff --git a/static/downloads/processed_b5fbd129_715c35b2804ba2593ad11520eaae65d8.jpg b/static/downloads/processed_b5fbd129_715c35b2804ba2593ad11520eaae65d8.jpg new file mode 100644 index 0000000..ea52579 Binary files /dev/null and b/static/downloads/processed_b5fbd129_715c35b2804ba2593ad11520eaae65d8.jpg differ diff --git a/static/downloads/processed_c79ac717_frieren_and_himmel_sousou_no_frieren_drawn_by_kagato007__fd9f47db3992105ea0f74228deb786df.jpg b/static/downloads/processed_c79ac717_frieren_and_himmel_sousou_no_frieren_drawn_by_kagato007__fd9f47db3992105ea0f74228deb786df.jpg new file mode 100644 index 0000000..5459d14 Binary files /dev/null and b/static/downloads/processed_c79ac717_frieren_and_himmel_sousou_no_frieren_drawn_by_kagato007__fd9f47db3992105ea0f74228deb786df.jpg differ diff --git a/static/downloads/processed_dd97df28_a40a1e293a660920f5d1f9249fc72da8.jpg b/static/downloads/processed_dd97df28_a40a1e293a660920f5d1f9249fc72da8.jpg new file mode 100644 index 0000000..bf8456b Binary files /dev/null and b/static/downloads/processed_dd97df28_a40a1e293a660920f5d1f9249fc72da8.jpg differ diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..9786e54 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,222 @@ + + + + + + Mobile Wallpaper Processor + + + + +
+
+

📱 Mobile Wallpaper Processor

+

Optimiere Bilder für 1440×3120 Displays (Pixel 6 Pro, Nothing Phone 3)

+
+ +
+ +
+ + + + + +

Bild hier ziehen oder klicken zum Auswählen

+

JPG, PNG, WEBP, GIF | max. 50MB

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

Automatische Optimierung: Smart Crop, AI Text-Entfernung, Kontrast & Schärfe

+
+
+ + + +