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.
Onur 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