Privacy-focused file conversion API built with FastAPI
Conversión de archivos privada, sin rastro, sin compromisos.
Easy Convert is a file conversion service designed with privacy as the core principle. Files are processed in memory, converted, and immediately deleted. No persistent storage, no content logging, no file analysis. The server doesn't know what's inside your documents.
| Category | What | Formats |
|---|---|---|
| Images | Convert, compress, remove BG, watermark, crop | JPEG, PNG, WebP, AVIF, HEIC, TIFF, BMP, GIF, SVG, +200 via ImageMagick |
| Audio | Convert, normalize volume, trim, bitrate/channels | MP3, WAV, FLAC, OGG, M4A, AAC, WMA, OPUS, AC3, AIFF |
| Video | Convert, compress (CRF/resolution/FPS), trim, extract/remove audio | MP4, WebM, AVI, MKV, MOV, GIF, OGG, FLV, TS |
| Merge, split, extract, delete/rotate pages, encrypt/decrypt, add text/image, draw, annotate | ||
| Documents | Convert between formats via Pandoc/LibreOffice | DOCX, PDF, ODT, EPUB, Markdown, HTML, LaTeX, and 30+ more |
| XML | Convert to JSON, YAML, HTML; transform via XSLT | XML |
- 📷 Images: docs/FASE_1_IMAGE_PROCESSING.md
- 📄 Documents/PDF/XML: docs/FASE_2_DOCUMENT_PROCESSING.md
This project follows Clean Architecture with Event Sourcing:
src/
├── domain/ # Core business logic (framework-agnostic)
│ └── job/ # Job aggregate, events, status
├── application/ # Use cases (commands, handlers, services)
├── infrastructure/ # External concerns (Redis, file storage, workers)
├── interfaces/ # API layer (HTTP controllers, WebSocket)
└── shared/ # Cross-cutting concerns
- FastAPI - Async Python web framework with OpenAPI
- Pydantic v2 - Data validation and settings
- Redis - Event store and job queue backend
- BullMQ - Distributed job queue system
- WebSockets - Real-time job status notifications
All processing tools pre-installed in the container:
# 1. Start services with docker-compose
docker-compose up -d
# 2. API available at http://localhost:8000
curl http://localhost:8000/health
# 3. View API docs
open http://localhost:8000/docs- Python 3.11+
- Redis server
- ImageMagick 7+
- Image processing tools (see Containerfile for installation)
- Install system dependencies
# Ubuntu/Debian
sudo apt-get install -y \
imagemagick \
jpegoptim \
pngquant \
git \
cmake \
nasm \
wget
# Build mozjpeg from source
git clone https://github.com/mozilla/mozjpeg.git
cd mozjpeg && mkdir build && cd build
cmake -G"Unix Makefiles" -DCMAKE_INSTALL_PREFIX=/opt/mozjpeg ..
make && sudo make install
# Download oxipng
wget https://github.com/shssoichiro/oxipng/releases/download/v9.1.2/oxipng-9.1.2-x86_64-unknown-linux-musl.tar.gz
tar xzf oxipng* && sudo mv oxipng /usr/local/bin/- Install Python dependencies
# Install uv if not already installed
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create venv and install dependencies
uv venv
source .venv/bin/activate
uv pip install -r pyproject.toml
# Or install with extras
uv pip install ".[dev]"- Configure environment
cp .env.example .env
# Edit .env with your Redis URL and settings- Start Redis
redis-server --appendonly yes# 1. Start API
uv run fastapi dev
# 2. Start worker (separate terminal)
uv run python src/infrastructure/worker/conversion_worker.py# Using Podman (recommended)
./scripts/compose.sh up -d
# Or using Docker Compose
docker-compose up -d
# Or using FastAPI CLI directly
uv run fastapi runOnce running, visit:
- Scalar UI: http://localhost:8000/scalar
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- OpenAPI JSON: http://localhost:8000/openapi.json
Full docs: docs/FASE_1_IMAGE_PROCESSING.md
- 200+ Format Conversions - ImageMagick: JPEG, PNG, WebP, AVIF, HEIC, TIFF, BMP, GIF, SVG
- Background Removal - AI-powered with rembg (U2Net, 100% local)
- Smart Compression - 3 levels (low/balanced/strong), mozjpeg/oxipng/pngquant/jpegoptim
- Watermarking - Text & logo, 6 positions + diagonal, opacity control
- Advanced Cropping - 4 modes (coordinates, aspect ratio, square, auto)
- Privacy Guaranteed - Auto EXIF stripping, local processing, no external APIs
Full docs: docs/FASE_2_DOCUMENT_PROCESSING.md
- PDF Manipulation — Merge, split, extract pages, rotate, metadata, encrypt/decrypt, add text/image, annotations, draw rectangles, adjustable layout
- Document Conversion — Pandoc + LibreOffice engines, auto-selection via MIME validation
- XML Conversion — XML → JSON / YAML / HTML / XSLT transform
- Audio Conversion (FFmpeg) — 10 input → 7 output, bitrate/sample rate/channels/trim/volume
- Video Conversion (FFmpeg) — 10 input → 8 output, CRF/resolution/FPS/trim/extract/remove audio
- Video Phase 2 (codec selection, watermark, ffprobe) — spec complete, pending
- File Upload Workflow — Create job → Upload (<10MB direct, >10MB chunked) → Start conversion
- Job Management — Status check, cancel, download (file deleted after download)
- Redis + BullMQ — Event store, job queue, persistence
- Rate Limiting — Redis sliding window, per-IP, configurable tiers (general/uploads), fail-open
- 69+ Tests — Unit + Integration across all phases
- OpenAPI Docs — Swagger UI + ReDoc auto-generated
# 1. Create job
curl -X POST http://localhost:8000/api/v1/upload/create \
-H "Content-Type: application/json" \
-d '{
"input_format": "jpg",
"output_formats": ["webp"],
"original_size": 1000000
}'
# → {"job_id": "abc123-...", "file_id": "..."}
# 2. Upload file
curl -X POST http://localhost:8000/api/v1/upload/abc123/file \
-F "file=@image.jpg"
# 3. Start conversion
curl -X POST http://localhost:8000/api/v1/upload/abc123/start
# 4. Check status
curl http://localhost:8000/api/v1/jobs/abc123
# 5. Download result (auto-deleted after download)
curl http://localhost:8000/api/v1/jobs/abc123/download -o output.webp# Full pipeline: remove BG + crop + compress + watermark
curl -X POST http://localhost:8000/api/v1/process/image \
-H "Content-Type: application/json" \
-d '{
"job_id": "abc123",
"output_format": "png",
"remove_background": true,
"crop": {"mode": "square", "square_size": 1080},
"compress": true, "compression_level": "balanced",
"watermark": {"type": "text", "text": "© 2026 Brand", "position": "bottom-right", "opacity": 0.7}
}'
# → 202 Accepted with pipeline configcurl -X POST http://localhost:8000/api/v1/process/audio \
-H "Content-Type: application/json" \
-d '{"job_id": "abc123", "output_format": "mp3", "bitrate": "192k", "channels": 2}'
# → 202 Accepted# Convert format
curl -X POST http://localhost:8000/api/v1/process/video \
-H "Content-Type: application/json" \
-d '{"job_id": "abc123", "output_format": "mkv", "crf": 23, "resolution": "1920:1080"}'
# Extract audio
curl -X POST http://localhost:8000/api/v1/process/video \
-H "Content-Type: application/json" \
-d '{"job_id": "abc123", "output_format": "mp3", "extract_audio": true, "audio_output_format": "mp3", "audio_bitrate": "192k"}'
# Remove audio track
curl -X POST http://localhost:8000/api/v1/process/video \
-H "Content-Type: application/json" \
-d '{"job_id": "abc123", "output_format": "mp4", "remove_audio": true}'# Merge PDFs
curl -X POST http://localhost:8000/api/v1/process/pdf/merge \
-H "Content-Type: application/json" \
-d '{"job_id": "abc123", "source_job_ids": ["pdf2", "pdf3"]}'
# Split pages (range)
curl -X POST http://localhost:8000/api/v1/process/pdf/split-range \
-H "Content-Type: application/json" \
-d '{"job_id": "abc123", "start_page": 1, "end_page": 5}'
# Encrypt
curl -X POST http://localhost:8000/api/v1/process/pdf/encrypt \
-H "Content-Type: application/json" \
-d '{"job_id": "abc123", "user_password": "secret123", "owner_password": "admin456"}'
# Full list: merge, split-range, extract-pages, delete-pages, rotate, metadata,
# encrypt, decrypt, add-text, add-image, draw-rectangle, add-annotation, set-mediabox# XML to JSON
curl -X POST http://localhost:8000/api/v1/convert/xml/json \
-F "file=@data.xml" \
-F "preserve_attributes=true"
# XML to YAML
curl -X POST http://localhost:8000/api/v1/convert/xml/yaml \
-F "file=@data.xml" \
-F "indent=2"
# XML to HTML (with template)
curl -X POST http://localhost:8000/api/v1/convert/xml/html \
-F "file=@data.xml" \
-F "template=table" \
-F "title=My Data"curl -X POST http://localhost:8000/api/v1/process/document \
-H "Content-Type: application/json" \
-d '{"job_id": "abc123", "output_format": "pdf", "preferred_engine": "auto"}'# Status
curl http://localhost:8000/api/v1/jobs/abc123
# Cancel
curl -X POST http://localhost:8000/api/v1/jobs/abc123/cancel \
-H "Content-Type: application/json" \
-d '{"reason": "user-requested"}'
# Download (file deleted after download)
curl http://localhost:8000/api/v1/jobs/abc123/download -o result.pdf📖 API Reference: Swagger UI · ReDoc
easy_convert_api/
├── src/
│ ├── domain/job/ # Domain entities, events, logic
│ ├── application/job/ # Commands, handlers, services
│ ├── infrastructure/ # Queue, storage, persistence, workers
│ ├── interfaces/ # HTTP & WebSocket endpoints
│ ├── main.py # FastAPI app
│ └── lifespan.py # App lifecycle management
├── shared/
│ ├── config/ # Settings and configuration
│ ├── events/ # Event bus
│ ├── queue/ # Queue abstractions
│ └── exceptions.py # Custom exceptions
├── tests/ # Test suite
├── pyproject.toml # Project dependencies
└── .env.example # Environment template
Complete test suite with 69 tests covering all phases.
# Run all tests (unit + integration, requires FFmpeg)
uv run pytest tests/ -v
# Run without integration tests (no FFmpeg needed)
uv run pytest tests/ --ignore=tests/test_audio_integration.py --ignore=tests/test_video_integration.py -v
# Run specific converter tests
uv run pytest tests/test_video_converter.py -v # 34 video unit tests
uv run pytest tests/test_audio_converter.py -v # audio unit tests
uv run pytest tests/test_image_converter.py -v # image unit tests
# With coverage report
uv run pytest tests/ --cov=src --cov=shared --cov-report=html📖 Full Testing Guide: docs/TESTING.md
The project follows Clean Code principles:
- Meaningful names
- Small, focused functions
- Single Responsibility Principle
- Dependency injection
- Type hints throughout
| Endpoint | Method | Description |
|---|---|---|
/upload/create |
POST | Create job (returns job_id + file_id) |
/upload/{job_id}/file |
POST | Upload complete file (<10MB) |
/upload/{job_id}/chunk |
POST | Upload chunk (>10MB, N times) |
/upload/{job_id}/merge |
POST | Merge chunks after upload |
/upload/{job_id}/start |
POST | Start conversion process |
/jobs/{job_id} |
GET | Get job status |
/jobs/{job_id}/cancel |
POST | Cancel pending/processing job |
/jobs/{job_id}/download |
GET | Download result (auto-deletes) |
/process/image |
POST | Full image pipeline |
/process/audio |
POST | Audio conversion |
/process/video |
POST | Video conversion |
/process/pdf/* |
POST | 13 PDF operations (merge, split, encrypt...) |
/process/document |
POST | Document conversion (auto engine) |
/convert/xml/* |
POST | XML → JSON/YAML/HTML/XSLT |
- ✅ Zero Persistent Storage - Files stored only in
/tmpwith random UUID names - ✅ Metadata Stripping - EXIF, IPTC, XMP automatically removed from all images
- ✅ Immediate Deletion - Files deleted immediately after download or conversion
- ✅ No Content Logging - Zero logging of file content, original names, or user data
- ✅ Local Processing - 100% local AI/compression (rembg, mozjpeg) - no external APIs
- ✅ Event Sourcing Audit - Complete processing history without storing files
- ✅ Time-Limited Metadata - Job metadata auto-expires after 24 hours
- ✅ Memory-Safe Operations - Async processing with proper cleanup
- ✅ No Analytics - Zero tracking, cookies, or user profiling
- ❌ Original file content
- ❌ File names (only job IDs)
- ❌ EXIF/GPS metadata
- ❌ User IP addresses
- ❌ Processing parameters after job completion
- ❌ Download history
- ✅ Job ID (UUID v4)
- ✅ Job status (queued/processing/completed/failed)
- ✅ File size and format
- ✅ Processing events (for debugging)
- ✅ Expires after: 24 hours
Your files, your business. Zero compromise on privacy.
The API uses a Redis sliding window algorithm for per-IP rate limiting. Zero external dependencies — built on the existing redis.asyncio connection pool.
| Tier | Limit | Window | Scope |
|---|---|---|---|
| General | 60 requests | 1 minute | All endpoints (default) |
| Uploads | 100 requests | 1 hour | /api/v1/upload/* |
Every response includes rate limit headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1753612800
When the limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header:
{
"error": "RateLimitError",
"message": "Rate limit exceeded"
}| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_ENABLED |
true |
Toggle to enable/disable rate limiting |
RATE_LIMIT_GENERAL_PER_MINUTE |
60 |
Requests per minute for general tier |
RATE_LIMIT_WINDOW_SECONDS |
60 |
Sliding window duration (seconds) |
RATE_LIMIT_UPLOADS_PER_HOUR |
100 |
Requests per hour for upload tier |
- Fail-open: If Redis is unavailable, requests pass through without rate limiting (availability > protection)
- OPTIONS passthrough: CORS preflight requests bypass rate limiting entirely
- Excluded paths:
/health,/,/docs,/redoc,/openapi.jsonare not rate limited - IP detection: Uses
X-Forwarded-Forheader (first value) for clients behind proxies, falls back toclient.host
Key environment variables (see .env.example):
MAX_FILE_SIZE_MB- Maximum file size (default: 100MB)JOB_TTL_HOURS- Job metadata retention (default: 24h)REDIS_URL- Redis connection URLTEMP_DIR- Temporary file storage location
Built with privacy in mind. Your files, your business.