Full-stack image generation application with FastAPI backend, Next.js frontend, and PostgreSQL database.
# Copy environment file and add your OpenRouter API key
cp .env.example .env
# Edit .env and set OPENROUTER_API_KEY
# Start all services (migrations run automatically on backend start)
docker-compose up -d
# View logs
docker-compose logs -f
# Stop services
docker-compose downPostgreSQL (~5 seconds):
✅ Ready: "database system is ready to accept connections"
Backend (~10-15 seconds):
✅ Ready: "✅ Database initialized"
✅ Ready: "✅ Database migrations applied"
✅ Ready: "✅ Async image worker started"
✅ Ready: "Uvicorn running on http://0.0.0.0:8000"
Frontend (~20-30 seconds):
✅ Ready: "✓ Ready in 4.5s"
✅ Ready: "Local: http://localhost:3000"
pgAdmin (~30-40 seconds):
✅ Ready: "Booting worker with pid"
✅ Ready: "Added 0 Server Group(s) and 1 Server(s)"
| Service | URL | Credentials |
|---|---|---|
| Frontend | http://localhost:3000 | - |
| Backend API | http://localhost:8000 | - |
| API Docs | http://localhost:8000/docs | - |
| MinIO Console | http://localhost:9001 | minioadmin:minioadmin |
| MinIO API | http://localhost:9000 | - |
| pgAdmin | http://localhost:5050 | admin@admin.com:admin |
Note: Images are automatically uploaded to MinIO S3 storage and returned as public HTTP URLs instead of base64 data URLs.
POST /generations
Generate images of random animals asynchronously using AI models. Images are automatically stored in MinIO S3 and returned as public HTTP URLs.
Request:
{
"numImages": 3
}Response:
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending"
}GET /generations/{jobId}
Get job status and generated image URLs.
Response:
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"numImages": 3,
"animal": "cat",
"imageUrls": [
"http://localhost:9000/generated-images/jobs/550e8400/image1.png",
"http://localhost:9000/generated-images/jobs/550e8400/image2.png",
"http://localhost:9000/generated-images/jobs/550e8400/image3.png"
],
"createdAt": "2025-12-12T10:30:00Z",
"updatedAt": "2025-12-12T10:30:15Z"
}POST /classify
Classify animals in an image using AI vision models. The system supports multiple providers with easy switching via configuration.
Request:
{
"imgUrl": "https://example.com/animal-image.jpg"
}Response:
{
"animals": ["cat", "dog"],
"error": null
}Example with cURL:
curl -X POST http://localhost:8000/classify \
-H "Content-Type: application/json" \
-d '{"imgUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"}'The classification endpoint uses an extensible provider system that allows easy switching between AI services:
| Provider | Configuration | API Key Required | Models Available |
|---|---|---|---|
| OpenRouter | VISION_PROVIDER=openrouter |
Yes (Get key) | GPT-4o, GPT-4o-mini, Claude, Gemini, etc. |
| Mock | VISION_PROVIDER=mock |
No | Pattern-based (for testing) |
Switching Providers:
Edit .env and set VISION_PROVIDER:
# Use OpenRouter (default, multiple models available)
VISION_PROVIDER=openrouter
VISION_MODEL=openai/gpt-4o-mini
OPENROUTER_API_KEY=sk-or-v1-your-key
# Use mock for testing (no API key needed)
VISION_PROVIDER=mockThen restart: docker-compose restart backend
The system uses MinIO - an S3-compatible object storage service - to convert base64-encoded images from AI providers into publicly accessible HTTP URLs.
- Image Generation: OpenRouter's Riverflow model generates images as base64 data URLs
- Automatic Upload: Background worker uploads images to MinIO S3 storage
- Public URLs: Images are accessible via HTTP URLs (e.g.,
http://localhost:9000/generated-images/...) - Organized Storage: Images stored in folders by job ID for easy management
Environment Variables (.env):
# Storage backend: "minio" or "none" (keeps base64)
STORAGE_BACKEND=minio
# MinIO S3 settings
MINIO_ENDPOINT=minio:9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=generated-images
MINIO_PUBLIC_URL=http://localhost:9000Access the MinIO web console at http://localhost:9001
Login: minioadmin / minioadmin
Features:
- Browse uploaded images by job ID
- View storage usage and metrics
- Manage buckets and access policies
- Download or delete images
- Monitor upload activity
generated-images/
├── jobs/
│ ├── 550e8400-e29b-41d4-a716-446655440000/
│ │ ├── abc123.png
│ │ ├── def456.png
│ │ └── ghi789.png
│ └── 661f9511-f3ac-52e5-b827-557766551111/
│ └── jkl012.png
└── test/
└── validation-images.png
To return base64 data URLs instead of HTTP URLs:
# In .env
STORAGE_BACKEND=noneThen restart: docker-compose restart backend
- Open http://localhost:5050
- Login:
admin@admin.com/admin - Expand "Servers" → Click "Sourceful PostgreSQL"
- Enter server password:
postgres(check "Save Password") - Navigate to: Databases → postgres → Schemas → public → Tables → jobs
Coverage: 77 tests, 87% overall
Test Files: (organized by type)
-
Unit Tests:
test_routes_unit.py- API endpoint teststest_services_unit.py- Business logic teststest_repository_unit.py- Database operation teststest_classify_unit.py- Vision classification unit tests (22 tests)
-
Integration Tests:
test_worker_integration.py- Async worker teststest_integration_e2e.py- End-to-end workflow teststest_classify_integration.py- Classification integration tests (7 tests)
Test Coverage by Component:
- Routes (87%) - API endpoints (create job, get job, list jobs, classify)
- Services (100%) - Business logic (job creation, status updates)
- Repositories (100%) - Database operations (CRUD, queries)
- Workers (82%) - Async job processing (concurrent execution)
- Providers (69%) - Unified provider system (OpenRouter, Mock)
- Integration (100%) - End-to-end workflows
Key Aspects Tested:
- ✅ Unified provider architecture (text-to-image + image-to-text)
- ✅ OpenRouter integration with Riverflow model
- ✅ Vision classification with multiple providers
- ✅ Provider factory pattern and easy switching
- ✅ Mock provider for testing without API costs
- ✅ Base64 to S3 URL conversion with MinIO
- ✅ URL validation and error handling
- ✅ Concurrent job processing with
asyncio.gather() - ✅ Async worker without threading conflicts
- ✅ Database session management with connection pooling
- ✅ Error handling and rollback mechanisms
- ✅ Job status transitions (pending → processing → completed/failed)
# Run all tests
docker exec sourceful-backend pytest
# Run with coverage report
docker exec sourceful-backend pytest --cov=. --cov-report=html
open backend/htmlcov/index.html
# Run specific test types
docker exec sourceful-backend pytest -m unit
docker exec sourceful-backend pytest -m integration
# Run specific test files
docker exec sourceful-backend pytest tests/test_classify_unit.py
docker exec sourceful-backend pytest tests/test_classify_integration.pyThe mobile category selection dialog displays the currently selected category with visual highlighting, allowing users to see their active selection while browsing all available options. This improves user awareness and provides clear context when choosing a new category.
Fixed an issue where the mobile dialog would remain visible when resizing from mobile to desktop view. The dialog now properly hides when switching to desktop breakpoint, ensuring clean transitions between mobile and desktop interfaces.
Implemented proper scroll navigation button visibility logic. The left navigation button now disappears when at the leftmost position, and the right navigation button disappears when at the rightmost position. This provides intuitive visual feedback about scroll boundaries and prevents unnecessary interaction with disabled navigation controls.
Fixed an unintentional layout glitch where clicking the "Product mockups" button caused the entire page to shift horizontally from left to right. This behavior was unique to this button and not present in other category buttons. The implementation now ensures consistent behavior across all category selections without any unwanted page movement.
Implemented minimum height constraint for the prompt box to ensure consistent sizing across all categories. Whether displaying a textarea input or info message badge, the prompt box now maintains a fixed height of 168px, preventing visual jumps when switching between different category types.
All AI providers extend from a single BaseProvider class supporting both capabilities:
- Text-to-Image: Generate images from text prompts
- Image-to-Text: Classify/analyze images
# Single provider instance for both tasks
provider = get_provider("openrouter",
api_key="...",
image_model="sourceful/riverflow-v2-max-preview",
vision_model="openai/gpt-4o-mini"
)
# Generate images
images = await provider.generate_images("a cute cat", 3)
# Classify images
result = await provider.classify_image(images[0])Benefits:
- Single configuration for all AI tasks
- Easy to add new providers
- Consistent error handling
- Simplified testing
Abstraction Layer: Worker doesn't need to know about storage implementation
# Worker simply generates images
image_urls = await provider.generate_images(prompt, num_images)
# Storage service handles conversion automatically
if settings.STORAGE_BACKEND == "minio":
image_urls = storage_service.upload_multiple_base64_images(image_urls)Flexibility: Switch between base64 and S3 URLs with a config change
- AsyncIO-based: No threading, pure async/await
- Database Pooling: Efficient connection management
- Error Recovery: Jobs marked as failed with error messages
- Concurrent Processing: Multiple jobs processed in parallel
# View container logs
docker logs sourceful-backend -f
docker logs sourceful-frontend -f
docker logs sourceful-minio -f
# Rebuild containers
docker-compose build --no-cache
docker-compose up -d
# Check container status
docker ps
# Reset MinIO storage
docker-compose down -v
docker-compose up -dImages not uploading to MinIO:
# Check MinIO is running
docker ps | grep minio
# Check MinIO logs
docker logs sourceful-minio
# Verify bucket exists
curl http://localhost:9000/minio/health/liveBase64 URLs instead of HTTP URLs:
- Check
STORAGE_BACKEND=minioin.env - Restart backend:
docker-compose restart backend
