99+ kostenlose Online-Tools, keine Installation, keine Registrierung. Das war die Vision fur die Tools-Zentrale. Die Herausforderung: Wie verarbeitet man PDFs, Videos, Audio und Bilder direkt im Browser – und nutzt gleichzeitig AI-Modelle wie Whisper oder Real-ESRGAN? Die Antwort: Eine Hybrid-Architektur mit 11 Docker-Microservices.
Das Problem: Komplexe Operationen ohne Software-Installation
Nutzeranforderungen
Unsere Zielgruppe wunscht sich:
- Keine Installation – Alles im Browser, sofort nutzbar
- Keine Registrierung – Datenschutz durch Anonymitat
- Keine Kosten – Kostenlose Nutzung ohne versteckte Gebuehren
- Professionelle Qualitat – Ergebnisse wie mit Desktop-Software
Die technische Realitat
Browser-basierte Verarbeitung hat Grenzen:
| Operation | Browser-Fahig | Grund |
|---|---|---|
| PDF zusammenfuegen | Ja | pdf-lib funktioniert client-side |
| PDF zu Word | Nein | LibreOffice benoetigt Server |
| Video komprimieren | Begrenzt | FFmpeg WASM ist langsam (5-10x) |
| AI Transkription | Nein | Whisper-Modelle zu gross (1-3GB) |
| Hintergrund entfernen | Nein | AI-Modelle benoetigen GPU/viel RAM |
| Bild-Upscaling | Nein | Real-ESRGAN zu ressourcenintensiv |
Erkenntnis: Wir brauchen eine Hybrid-Losung – client-side wo moeglich, server-side wo noetig.
Architektur-Entscheidungen
Warum Hybrid-Processing?
Die Entscheidung fiel auf eine zweistufige Architektur:
Nutzer-Browser
|
v
+--------------------+
| Next.js Frontend |
| (React 19 + WASM) |
+--------------------+
|
+---> Client-side: pdf-lib, FFmpeg WASM, Tesseract.js
|
+---> Server-side: Docker Microservices
|
v
+---------------+
| Redis/BullMQ | <-- Job Queue
+---------------+
|
+-----------+-----------+-----------+
| | | |
v v v v
+-------+ +--------+ +-------+ +-----+
|Whisper| | rembg | |ESRGAN | | ... |
+-------+ +--------+ +-------+ +-----+
Vorteile:
- Schnelle Operationen (PDF merge, Text-Tools) laufen instant im Browser
- Rechenintensive AI-Operationen auf dedizierten Servern
- Automatisches Fallback bei Service-Ausfall
- Skalierbar: Services unabhaengig skalierbar
Warum Docker Microservices?
Jeder Service hat spezifische Anforderungen:
| Service | Base Image | Grund |
|---|---|---|
| pdf-converter | Node + LibreOffice | LibreOffice benoetigt vollstaendige Installation |
| whisper | Python + PyTorch | GPU-optimierte AI-Inferenz |
| rembg | Python + ONNX | Spezialisiertes AI-Modell |
| esrgan | Python + PyTorch | GPU-beschleunigtes Upscaling |
| ffmpeg | Node + FFmpeg | Native Video-Encoding |
| tesseract | Python + Tesseract | OCR mit trainierten Modellen |
| tts | Python + Coqui TTS | Text-to-Speech Synthese |
| libretranslate | Python + LibreTranslate | Neurale Ubersetzung |
Isolation: Jeder Service hat eigene Dependencies – keine Konflikte zwischen Python-Versionen oder Library-Inkompatibilitaeten.
Die Service-Landschaft
# docker-compose.yml (vereinfacht)
services:
pdf-converter:
build: ./docker/pdf-converter
ports: ["3099:3099"]
mem_limit: 1g
depends_on: [redis]
whisper:
build: ./docker/whisper
ports: ["3100:3100"]
mem_limit: 2g
# GPU-Support wenn verfuegbar
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]
rembg:
build: ./docker/rembg
ports: ["3101:3101"]
mem_limit: 1g
esrgan:
build: ./docker/esrgan
ports: ["3102:3102"]
mem_limit: 1g
# ... weitere Services
redis:
image: redis:alpine
ports: ["6379:6379"]
mem_limit: 256mJob-Queue mit Redis und BullMQ
Das Problem der langen Verarbeitung
AI-Operationen dauern 5-60 Sekunden. HTTP-Timeouts und User-Experience werden zum Problem.
Die Loesung: Asynchrone Job-Queue
// lib/api/server-processing.ts (vereinfacht)
export async function submitPdfJob(
file: File,
operation: 'convert' | 'compress' | 'ocr'
): Promise<string> {
const formData = new FormData();
formData.append('file', file);
formData.append('operation', operation);
const response = await fetch('/api/pdf/jobs', {
method: 'POST',
body: formData,
});
const { jobId } = await response.json();
return jobId; // Sofortige Antwort
}
export async function getJobStatus(jobId: string): Promise<JobStatus> {
const response = await fetch(`/api/pdf/jobs/${jobId}/status`);
return response.json();
}Flow:
- Nutzer laedt Datei hoch
- Server erstellt Job in Redis-Queue
- Worker nimmt Job entgegen
- Frontend pollt Status alle 1-2 Sekunden
- Bei Fertigstellung: Download-Link bereit
Custom Hooks fuer Developer Experience
useQueueJob: Polling und Status-Tracking
// hooks/useQueueJob.ts
export function useQueueJob(options: UseQueueJobOptions = {}): UseQueueJobReturn {
const { pollInterval = 1000, autoDownload = true, onComplete, onError } = options;
const [jobId, setJobId] = useState<string | null>(null);
const [status, setStatus] = useState<JobStatus | null>(null);
const [progress, setProgress] = useState(0);
const [position, setPosition] = useState(0);
const [estimatedTime, setEstimatedTime] = useState<number | null>(null);
// Polling-Logik
useEffect(() => {
if (!jobId || !isPolling) return;
const poll = async () => {
const jobStatus = await getJobStatus(jobId);
setStatus(jobStatus.status);
setProgress(jobStatus.progress);
setPosition(jobStatus.position);
if (jobStatus.status === 'completed') {
setIsPolling(false);
if (autoDownload) {
const result = await downloadJobResult(jobId);
// Browser-Download triggern
triggerDownload(result.blob, result.filename);
onComplete?.(result.blob, result.filename);
}
return;
}
if (jobStatus.status === 'failed') {
setIsPolling(false);
onError?.(new Error(jobStatus.error));
return;
}
// Weiter pollen
pollingRef.current = setTimeout(poll, pollInterval);
};
poll();
}, [jobId, isPolling]);
return {
status,
progress,
position,
estimatedTime,
startJob: (id: string) => { setJobId(id); setIsPolling(true); },
cancel: () => cancelJob(jobId),
reset: () => { /* Reset state */ },
};
}Verwendung in Komponenten:
function PdfToWordTool() {
const { status, progress, position, startJob } = useQueueJob({
onComplete: (blob, filename) => {
toast.success(`${filename} erfolgreich konvertiert!`);
},
onError: (error) => {
toast.error(error.message);
},
});
const handleUpload = async (file: File) => {
const jobId = await submitPdfJob(file, 'convert');
startJob(jobId);
};
return (
<div>
{status === 'waiting' && (
<p>Position in Warteschlange: {position}</p>
)}
{status === 'active' && (
<ProgressBar value={progress} />
)}
</div>
);
}useBatchJob: Mehrere Dateien parallel
// hooks/useBatchJob.ts
export function useBatchJob(options: UseBatchJobOptions = {}): UseBatchJobReturn {
const [totalFiles, setTotalFiles] = useState(0);
const [completedFiles, setCompletedFiles] = useState(0);
const [failedFiles, setFailedFiles] = useState(0);
const [files, setFiles] = useState<BatchFileInfo[]>([]);
// Batch-Status enthalt Details pro Datei
useEffect(() => {
if (!batchId || !isPolling) return;
const poll = async () => {
const batchStatus = await getBatchStatus(batchId);
setTotalFiles(batchStatus.totalFiles);
setCompletedFiles(batchStatus.completedFiles);
setFailedFiles(batchStatus.failedFiles);
setFiles(batchStatus.files); // Status jeder einzelnen Datei
if (batchStatus.status === 'completed') {
// ZIP-Download mit allen Ergebnissen
const result = await downloadBatchResult(batchId);
triggerDownload(result.blob, 'converted-files.zip');
}
};
poll();
}, [batchId, isPolling]);
return {
progress: Math.round((completedFiles / totalFiles) * 100),
totalFiles,
completedFiles,
failedFiles,
files, // Fuer detaillierte Fortschrittsanzeige
startBatch,
cancel,
};
}useFFmpeg: Client-side Video-Processing
// hooks/useFFmpeg.ts
export function useFFmpeg(): UseFFmpegReturn {
const [status, setStatus] = useState<FFmpegStatus>('unloaded');
const [progress, setProgress] = useState(0);
const ffmpegRef = useRef<FFmpeg | null>(null);
const load = useCallback(async () => {
setStatus('loading');
const { FFmpeg } = await import('@ffmpeg/ffmpeg');
const { toBlobURL } = await import('@ffmpeg/util');
const ffmpeg = new FFmpeg();
// Progress-Events
ffmpeg.on('progress', ({ progress: p }) => {
setProgress(Math.round(p * 100));
});
// WASM-Core von CDN laden (nur einmal)
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.15/dist/esm';
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
});
ffmpegRef.current = ffmpeg;
setStatus('ready');
}, []);
const process = useCallback(async (
inputFiles: { name: string; data: Uint8Array }[],
command: string[],
outputFilename: string
): Promise<Uint8Array | null> => {
const ffmpeg = ffmpegRef.current!;
// Dateien ins WASM-Filesystem schreiben
for (const file of inputFiles) {
await ffmpeg.writeFile(file.name, file.data);
}
// FFmpeg-Befehl ausfuehren
await ffmpeg.exec(command);
// Ergebnis lesen
const data = await ffmpeg.readFile(outputFilename);
// Cleanup: Temporaere Dateien loeschen
for (const file of inputFiles) {
await ffmpeg.deleteFile(file.name);
}
return data as Uint8Array;
}, []);
return { status, progress, load, process, cancel };
}Beispiel: Video zu GIF
function VideoToGifTool() {
const { status, progress, load, process } = useFFmpeg();
const convertToGif = async (videoFile: File) => {
// FFmpeg WASM laden (lazy loading)
if (status === 'unloaded') await load();
const videoData = new Uint8Array(await videoFile.arrayBuffer());
// FFmpeg-Befehl: Video zu GIF
const gifData = await process(
[{ name: 'input.mp4', data: videoData }],
[
'-i', 'input.mp4',
'-vf', 'fps=10,scale=480:-1:flags=lanczos',
'-loop', '0',
'output.gif'
],
'output.gif'
);
// Download triggern
const blob = new Blob([gifData!], { type: 'image/gif' });
downloadBlob(blob, 'converted.gif');
};
return (
<div>
{status === 'loading' && <p>FFmpeg wird geladen...</p>}
{status === 'processing' && <ProgressBar value={progress} />}
</div>
);
}AI-Integration: Die Service-Clients
Whisper: Audio-Transkription
// lib/api/ai-services.ts
export async function transcribeAudio(
file: File,
options?: {
language?: string;
task?: 'transcribe' | 'translate';
}
): Promise<TranscriptionResult> {
const formData = new FormData();
formData.append('file', file);
if (options?.language) formData.append('language', options.language);
if (options?.task) formData.append('task', options.task);
const response = await fetch('/api/ai/transcribe', {
method: 'POST',
body: formData,
});
return response.json();
}
// Rueckgabe
interface TranscriptionResult {
text: string;
language: string;
segments?: Array<{
start: number;
end: number;
text: string;
}>;
}Docker-Service (vereinfacht):
# docker/whisper/Dockerfile
FROM python:3.11-slim
# System-Dependencies fuer Audio
RUN apt-get update && apt-get install -y ffmpeg
# Whisper installieren
RUN pip install openai-whisper torch
# Modell beim Build laden (1.5GB)
RUN python -c "import whisper; whisper.load_model('base')"
COPY app.py .
CMD ["python", "app.py"]rembg: Hintergrund-Entfernung
export async function removeBackground(
file: File,
options?: {
alphaMatting?: boolean;
onlyMask?: boolean;
bgcolor?: string;
}
): Promise<Blob> {
const formData = new FormData();
formData.append('file', file);
if (options?.alphaMatting) formData.append('alpha_matting', 'true');
if (options?.bgcolor) formData.append('bgcolor', options.bgcolor);
const response = await fetch('/api/ai/remove-background', {
method: 'POST',
body: formData,
});
return response.blob();
}Was rembg macht:
- U2-Net AI-Modell fuer Segmentierung
- Alpha Matting fuer feine Kanten (Haare)
- Optional: Neuen Hintergrund einfuegen
Real-ESRGAN: Bild-Upscaling
export async function upscaleImage(
file: File,
options?: {
scale?: 2 | 3 | 4;
model?: 'general' | 'anime';
}
): Promise<UpscaleResult> {
const formData = new FormData();
formData.append('file', file);
if (options?.scale) formData.append('scale', options.scale.toString());
if (options?.model) formData.append('model', options.model);
const response = await fetch('/api/ai/upscale', {
method: 'POST',
body: formData,
});
return {
blob: await response.blob(),
originalSize: response.headers.get('X-Original-Size'),
outputSize: response.headers.get('X-Output-Size'),
processingTime: response.headers.get('X-Processing-Time'),
};
}Error Handling und Fallbacks
Service-Health-Checks
// Vor jeder Anfrage pruefen ob Service verfuegbar
export async function getTranscribeHealth(): Promise<HealthStatus> {
try {
const response = await fetch('/api/ai/transcribe');
return response.json();
} catch {
return { status: 'unavailable', error: 'Service nicht erreichbar' };
}
}
// In der Komponente
function AudioToTextTool() {
const [serviceAvailable, setServiceAvailable] = useState<boolean | null>(null);
useEffect(() => {
getTranscribeHealth().then((health) => {
setServiceAvailable(health.status === 'ok');
});
}, []);
if (serviceAvailable === false) {
return (
<Alert variant="warning">
Transkriptions-Service derzeit nicht verfuegbar.
Bitte spaeter erneut versuchen.
</Alert>
);
}
}Client-side Fallbacks
// Beispiel: Hintergrund-Entfernung
async function removeBackgroundWithFallback(file: File): Promise<Blob> {
// Zuerst Server-Service versuchen
const health = await getRemoveBackgroundHealth();
if (health.status === 'ok') {
try {
return await removeBackground(file);
} catch (error) {
console.warn('Server-Service fehlgeschlagen, fallback auf Client');
}
}
// Fallback: @imgly/background-removal (client-side, langsamer)
const { removeBackground: clientRemove } = await import('@imgly/background-removal');
const blob = await clientRemove(file, {
model: 'small',
output: { format: 'image/png' },
});
return blob;
}Memory Management fuer AI-Services
Das Problem
AI-Modelle sind speicherhungrig:
- Whisper base: 1.5GB VRAM/RAM
- Real-ESRGAN: 500MB-2GB je nach Bildgroesse
- rembg (U2-Net): 300MB
Die Loesung: Resource Limits
# docker-compose.yml
services:
whisper:
mem_limit: 2g
memswap_limit: 2g
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 1G
esrgan:
mem_limit: 1g
# Bildgroesse serverseitig limitieren
environment:
- MAX_IMAGE_SIZE=4096
- MAX_FILE_SIZE_MB=20Server-seitige Validierung:
# docker/esrgan/app.py
MAX_IMAGE_SIZE = int(os.getenv('MAX_IMAGE_SIZE', 4096))
MAX_FILE_SIZE_MB = int(os.getenv('MAX_FILE_SIZE_MB', 20))
@app.post('/upscale')
async def upscale(file: UploadFile):
# Dateigroesse pruefen
file_size = len(await file.read())
if file_size > MAX_FILE_SIZE_MB * 1024 * 1024:
raise HTTPException(400, f'Datei zu gross (max {MAX_FILE_SIZE_MB}MB)')
# Bildgroesse pruefen
image = Image.open(file.file)
if max(image.size) > MAX_IMAGE_SIZE:
raise HTTPException(400, f'Bild zu gross (max {MAX_IMAGE_SIZE}px)')
# Verarbeitung...Performance-Optimierung
WASM Lazy Loading
FFmpeg WASM ist 25MB gross – nicht fuer jeden Nutzer laden:
// Nur laden wenn Tool verwendet wird
const VideoTool = dynamic(() => import('./VideoTool'), {
loading: () => <ToolSkeleton />,
ssr: false, // WASM funktioniert nicht server-side
});
// Im useFFmpeg Hook: Lazy load der WASM-Module
const load = useCallback(async () => {
// Dynamischer Import – nur wenn benoetigt
const { FFmpeg } = await import('@ffmpeg/ffmpeg');
const { toBlobURL } = await import('@ffmpeg/util');
// Core von CDN laden
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.15/dist/esm';
// ...
}, []);Service Worker Caching
// app/manifest.ts
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'Tools-Zentrale',
short_name: 'Tools',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#3b82f6',
icons: [/* ... */],
};
}
// next.config.ts
const withPWA = require('next-pwa')({
dest: 'public',
disable: process.env.NODE_ENV === 'development',
runtimeCaching: [
{
// WASM-Dateien cachen
urlPattern: /\.wasm$/,
handler: 'CacheFirst',
options: {
cacheName: 'wasm-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Tage
},
},
},
],
});CLS Prevention
Layout-Shifts vermeiden bei Tool-Loading:
/* globals.css */
.tool-dropzone {
min-height: 200px;
}
.tool-dropzone-lg {
min-height: 280px;
}
.tool-result-container {
min-height: 120px;
}
.tool-main-area {
min-height: 320px;
}Lessons Learned
1. Hybrid ist der Weg
Weder reines Client-side noch reines Server-side ist optimal:
- Client-side: Schnell, privat, aber limitiert
- Server-side: Maechtig, aber teuer und langsamer
- Hybrid: Das Beste aus beiden Welten
2. Job-Queues sind essentiell
Ohne BullMQ haetten wir:
- HTTP-Timeouts bei langen Operationen
- Keine Warteschlangen-Logik
- Keine Retry-Moeglichkeit bei Fehlern
- Schlechte UX (kein Fortschritt sichtbar)
3. Docker Isolation zahlt sich aus
Anfangs uebertrieben? Heute unverzichtbar:
- Whisper braucht spezifische PyTorch-Version
- LibreOffice hat komplexe System-Dependencies
- Updates einzelner Services ohne Ausfallzeit
4. Memory Management nicht unterschaetzen
AI-Modelle sind hungrig:
- Immer Limits setzen
- Bildgroessen validieren
- Health-Checks implementieren
- Monitoring einrichten
5. Graceful Degradation
Wenn ein Service ausfallt:
- Nutzer informieren
- Fallback anbieten (wenn moeglich)
- Nicht die ganze App blockieren
Ergebnisse
| Metrik | Ziel | Erreicht |
|---|---|---|
| Tools online | 50+ | 99+ |
| Durchschnittliche Verarbeitung | < 10s | 3-8s |
| LCP | < 2.5s | 1.4s |
| CLS | < 0.1 | 0.03 |
| Service Uptime | 99% | 99.7% |
| Memory Peaks | < 2GB | < 1.8GB |
Tech-Stack Zusammenfassung
| Komponente | Technologie | Zweck |
|---|---|---|
| Frontend | Next.js 16, React 19 | App-Framework |
| Styling | Tailwind CSS 4 | Responsive Design |
| State | Zustand | Client-State Management |
| PDF Client | pdf-lib, pdfjs-dist | Client-side PDF |
| Video Client | FFmpeg WASM | Client-side Video |
| Job Queue | Redis, BullMQ | Async Processing |
| PDF Server | LibreOffice, qpdf | Server-side Konvertierung |
| AI Transcription | OpenAI Whisper | Audio zu Text |
| AI Background | rembg (U2-Net) | Hintergrund-Entfernung |
| AI Upscaling | Real-ESRGAN | Bild-Vergroesserung |
| AI TTS | Coqui TTS | Text zu Sprache |
| Translation | LibreTranslate | Neurale Ubersetzung |
| Container | Docker Compose | Service-Orchestrierung |
Naechste Schritte
Die Tools-Zentrale waechst weiter:
- Mehr AI-Tools: Gesichtserkennung, Style Transfer
- GPU-Beschleunigung: NVIDIA Container Toolkit
- API-Zugang: REST-API fuer Entwickler
- Premium-Tier: Hoehere Limits, Priority-Queue
Sie planen ein komplexes Web-Projekt? Die Tools-Zentrale zeigt, wie wir skalierbare Hybrid-Architekturen mit AI-Integration umsetzen. Kontaktieren Sie uns fuer ein technisches Beratungsgespraech zu Ihrem Vorhaben.
Kostenlose Erstberatung
Lassen Sie uns über Ihr Projekt sprechen. Unverbindlich und kostenfrei.
Beratungstermin buchenOnur Cirakoglu ist Gründer und leitender Entwickler von HEADON.pro. Mit über 8 Jahren Erfahrung in der Webentwicklung spezialisiert er sich auf performante Next.js-Anwendungen, React Native Mobile Apps und komplexe Full-Stack-Lösungen. Seine Expertise umfasst moderne JavaScript-Frameworks, Cloud-Architekturen und SEO-optimierte Webanwendungen. Er berät Unternehmen im Main-Tauber-Kreis und darüber hinaus bei ihrer digitalen Transformation.
Expertise
Das könnte Sie auch interessieren
Weitere Artikel zu ähnlichen Themen
Vorlagen-Zentrale: Von Idee zur Web-App
Einblick in unseren Entwicklungsprozess: Von der Marktanalyse über UX-Design bis zum Launch einer Dokumenten-Plattform mit E-Rechnungs-Generator.
WeiterlesenDSGVO-konforme Finanz-Web-App | Case Study
Von der Idee zum Launch: Technische Deep-Dive in die Entwicklung der Rechner Zentrale mit 36+ Finanz-Rechnern, Privacy-First-Architektur und SEO-Strategie.
WeiterlesenProgressive Web Apps (PWA): Guide 2025
Progressive Web Apps (PWA) verstehen: Vorteile gegenüber Native Apps, Service Workers, Offline-Funktionalität und Installation auf dem Homescreen.
Weiterlesen