Spaces:
Sleeping
Sleeping
Complete PhotoGuard rebrand and API documentation
Browse files- Rebrand from 'Civic Photo Quality Control' to 'PhotoGuard'
- Convert UI theme to professional black/white/gray monochrome palette
- Add comprehensive OpenAPI 3.1 specification
- Implement interactive Swagger UI documentation (/api/docs)
- Add ReDoc API reference documentation (/api/redoc)
- Remove geographic validation and unrelated code
- Clean up project structure (removed 15+ unnecessary files)
- Update all descriptions to be generic and professional
- Add new API endpoints for documentation serving
- Update README with new branding and documentation links
- Version bump to 3.0.0
- .dockerignore +44 -0
- .gitignore +4 -8
- CHANGELOG.md +0 -162
- Dockerfile +3 -6
- QUICK_START.md +0 -271
- README.md +15 -26
- api_test.py +0 -187
- app/__init__.py +24 -17
- app/api_spec.py +542 -0
- app/routes/upload.py +93 -220
- app/services/quality_control.py +2 -53
- app/utils/blur_detection.py +30 -1
- app/utils/image_validation.py +0 -77
- app/utils/metadata_extraction.py +0 -30
- app/utils/object_detection.py +0 -140
- config.py +9 -27
- docker-compose.yml +0 -41
- docs/API.md +0 -38
- docs/API_v2.md +0 -427
- docs/DEPLOYMENT.md +0 -606
- docs/DEPLOYMENT_CHECKLIST.md +0 -351
- models/.gitkeep +0 -0
- models/__init__.py +0 -1
- models/model_loader.py +0 -33
- nginx/nginx.conf +0 -89
- production.py +0 -153
- requirements.txt +15 -12
- scripts/download_models.py +0 -32
- scripts/setup_directories.py +0 -27
- start_production.bat +0 -74
- start_production.sh +0 -65
- templates/index.html +806 -0
- templates/mobile_upload.html +0 -624
- templates/redoc.html +70 -0
- templates/swagger.html +96 -0
- tests/test_api_endpoints.py +56 -17
.dockerignore
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
*.egg-info/
|
| 8 |
+
.pytest_cache/
|
| 9 |
+
.mypy_cache/
|
| 10 |
+
|
| 11 |
+
# Virtual environments
|
| 12 |
+
.venv/
|
| 13 |
+
venv/
|
| 14 |
+
env/
|
| 15 |
+
ENV/
|
| 16 |
+
|
| 17 |
+
# Development
|
| 18 |
+
.git/
|
| 19 |
+
.gitignore
|
| 20 |
+
.vscode/
|
| 21 |
+
.idea/
|
| 22 |
+
.DS_Store
|
| 23 |
+
*.log
|
| 24 |
+
|
| 25 |
+
# Storage (runtime generated)
|
| 26 |
+
storage/temp/*
|
| 27 |
+
storage/processed/*
|
| 28 |
+
storage/rejected/*
|
| 29 |
+
logs/*
|
| 30 |
+
|
| 31 |
+
# Documentation & tests
|
| 32 |
+
docs/
|
| 33 |
+
tests/
|
| 34 |
+
*.md
|
| 35 |
+
CHANGELOG.md
|
| 36 |
+
QUICK_START.md
|
| 37 |
+
|
| 38 |
+
# CI/CD
|
| 39 |
+
.github/
|
| 40 |
+
|
| 41 |
+
# Local dev files
|
| 42 |
+
api_test.py
|
| 43 |
+
start_production.sh
|
| 44 |
+
start_production.bat
|
.gitignore
CHANGED
|
@@ -75,11 +75,7 @@ dmypy.json
|
|
| 75 |
.idea/
|
| 76 |
.DS_Store
|
| 77 |
Thumbs.db
|
| 78 |
-
|
| 79 |
-
storage/
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
!storage/processed/.gitkeep
|
| 83 |
-
!storage/rejected/.gitkeep
|
| 84 |
-
models/*.pt
|
| 85 |
-
!models/.gitkeep
|
|
|
|
| 75 |
.idea/
|
| 76 |
.DS_Store
|
| 77 |
Thumbs.db
|
| 78 |
+
# Auto-created directories (app creates these automatically)
|
| 79 |
+
storage/
|
| 80 |
+
logs/
|
| 81 |
+
*.log
|
|
|
|
|
|
|
|
|
|
|
|
CHANGELOG.md
DELETED
|
@@ -1,162 +0,0 @@
|
|
| 1 |
-
# Changelog - Civic Photo Quality Control API
|
| 2 |
-
|
| 3 |
-
All notable changes to this project will be documented in this file.
|
| 4 |
-
|
| 5 |
-
## [2.0.0] - September 26, 2025
|
| 6 |
-
|
| 7 |
-
### 🎉 Major Release - Production Ready
|
| 8 |
-
|
| 9 |
-
#### Added
|
| 10 |
-
- **Weighted Scoring System**: Intelligent partial credit validation
|
| 11 |
-
- Blur Detection: 25% weight
|
| 12 |
-
- Resolution Check: 25% weight
|
| 13 |
-
- Brightness Validation: 20% weight
|
| 14 |
-
- Exposure Analysis: 15% weight
|
| 15 |
-
- Metadata Extraction: 15% weight
|
| 16 |
-
- Pass threshold: 65% overall score
|
| 17 |
-
|
| 18 |
-
- **Mobile-Optimized Validation Rules**:
|
| 19 |
-
- Blur threshold: 100 (down from 150)
|
| 20 |
-
- Brightness range: 50-220 (expanded from 90-180)
|
| 21 |
-
- Resolution: 800×600 minimum (down from 1024×1024)
|
| 22 |
-
- Metadata requirement: 15% (down from 30%)
|
| 23 |
-
- Exposure tolerance: Increased flexibility
|
| 24 |
-
|
| 25 |
-
- **Comprehensive API Endpoints**:
|
| 26 |
-
- `GET /api/health` - System health check
|
| 27 |
-
- `POST /api/validate` - Primary image validation
|
| 28 |
-
- `GET /api/summary` - Processing statistics
|
| 29 |
-
- `GET /api/validation-rules` - Current thresholds
|
| 30 |
-
- `GET /api/test-api` - API information
|
| 31 |
-
- `POST /api/upload` - Legacy endpoint (deprecated)
|
| 32 |
-
|
| 33 |
-
- **Complete Documentation Suite**:
|
| 34 |
-
- README.md - Comprehensive project overview
|
| 35 |
-
- QUICK_START.md - 60-second deployment guide
|
| 36 |
-
- docs/API_v2.md - Full API documentation
|
| 37 |
-
- docs/DEPLOYMENT.md - Production deployment guide
|
| 38 |
-
- docs/DEPLOYMENT_CHECKLIST.md - Step-by-step deployment
|
| 39 |
-
|
| 40 |
-
#### Changed
|
| 41 |
-
- **Acceptance Rate Improvement**: 16.67% → 35-40% (132% increase)
|
| 42 |
-
- **Response Format**: New structured JSON with summary and detailed checks
|
| 43 |
-
- **Configuration**: Centralized in config.py with comprehensive comments
|
| 44 |
-
- **Error Handling**: Enhanced with detailed error messages and recommendations
|
| 45 |
-
|
| 46 |
-
#### Improved
|
| 47 |
-
- **Code Documentation**: Comprehensive docstrings and inline comments
|
| 48 |
-
- **Configuration Clarity**: Detailed explanations of all validation rules
|
| 49 |
-
- **Production Readiness**: Enhanced deployment scripts and logging
|
| 50 |
-
- **Mobile Compatibility**: Optimized thresholds for smartphone photography
|
| 51 |
-
|
| 52 |
-
#### Removed
|
| 53 |
-
- Outdated test files (create_and_test.py, direct_test.py, etc.)
|
| 54 |
-
- Windows-specific batch files
|
| 55 |
-
- Duplicate configuration files (production.yaml, .env)
|
| 56 |
-
- Obsolete API documentation (docs/API.md)
|
| 57 |
-
|
| 58 |
-
### 📊 Performance Metrics
|
| 59 |
-
|
| 60 |
-
- **Acceptance Rate**: 35-40% (target achieved)
|
| 61 |
-
- **Processing Time**: <2 seconds per image
|
| 62 |
-
- **API Response Time**: <500ms for health checks
|
| 63 |
-
- **Supported Formats**: JPG, JPEG, PNG, BMP, TIFF
|
| 64 |
-
- **Maximum File Size**: 32MB
|
| 65 |
-
|
| 66 |
-
### 🔧 Technical Details
|
| 67 |
-
|
| 68 |
-
#### Validation Components
|
| 69 |
-
1. **Blur Detection** (25%)
|
| 70 |
-
- Method: Laplacian variance analysis
|
| 71 |
-
- Threshold: 100 minimum
|
| 72 |
-
- Levels: Poor (0-99), Acceptable (100-299), Excellent (300+)
|
| 73 |
-
|
| 74 |
-
2. **Resolution Check** (25%)
|
| 75 |
-
- Minimum: 800×600 pixels (0.5MP)
|
| 76 |
-
- Recommended: 2+ megapixels
|
| 77 |
-
- Supports landscape and portrait orientations
|
| 78 |
-
|
| 79 |
-
3. **Brightness Validation** (20%)
|
| 80 |
-
- Range: 50-220 pixel intensity
|
| 81 |
-
- Method: Histogram analysis
|
| 82 |
-
- Quality threshold: 60% minimum
|
| 83 |
-
|
| 84 |
-
4. **Exposure Analysis** (15%)
|
| 85 |
-
- Dynamic range: 80-150 acceptable
|
| 86 |
-
- Clipping check: Max 2% clipped pixels
|
| 87 |
-
- Method: Pixel distribution analysis
|
| 88 |
-
|
| 89 |
-
5. **Metadata Extraction** (15%)
|
| 90 |
-
- Required completeness: 15%
|
| 91 |
-
- Key fields: Timestamp, camera info, settings
|
| 92 |
-
- EXIF analysis with GPS validation
|
| 93 |
-
|
| 94 |
-
### 🚀 Deployment
|
| 95 |
-
|
| 96 |
-
#### Docker Deployment (Recommended)
|
| 97 |
-
```bash
|
| 98 |
-
docker-compose up -d
|
| 99 |
-
```
|
| 100 |
-
|
| 101 |
-
#### Manual Deployment
|
| 102 |
-
```bash
|
| 103 |
-
pip install -r requirements.txt
|
| 104 |
-
python scripts/setup_directories.py
|
| 105 |
-
python scripts/download_models.py
|
| 106 |
-
gunicorn --bind 0.0.0.0:8000 --workers 4 production:app
|
| 107 |
-
```
|
| 108 |
-
|
| 109 |
-
### 📚 Documentation
|
| 110 |
-
|
| 111 |
-
- **API Documentation**: Comprehensive endpoint documentation with examples
|
| 112 |
-
- **Deployment Guide**: Step-by-step production deployment instructions
|
| 113 |
-
- **Quick Start**: 60-second getting started guide
|
| 114 |
-
- **Configuration Reference**: Detailed explanation of all settings
|
| 115 |
-
|
| 116 |
-
### 🔒 Security
|
| 117 |
-
|
| 118 |
-
- File type validation (images only)
|
| 119 |
-
- Size limits enforced (32MB maximum)
|
| 120 |
-
- Input sanitization on all uploads
|
| 121 |
-
- Automatic temporary file cleanup
|
| 122 |
-
- Environment variable secrets
|
| 123 |
-
- No sensitive data in error responses
|
| 124 |
-
|
| 125 |
-
### 🧪 Testing
|
| 126 |
-
|
| 127 |
-
- Comprehensive API test suite (api_test.py)
|
| 128 |
-
- Unit tests for individual components
|
| 129 |
-
- Sample images for validation testing
|
| 130 |
-
- Production testing checklist included
|
| 131 |
-
|
| 132 |
-
### 📈 Future Enhancements
|
| 133 |
-
|
| 134 |
-
- [ ] Real-time processing optimization
|
| 135 |
-
- [ ] Advanced object detection integration
|
| 136 |
-
- [ ] Batch processing capabilities
|
| 137 |
-
- [ ] API rate limiting
|
| 138 |
-
- [ ] Enhanced mobile UI
|
| 139 |
-
- [ ] Multi-language support
|
| 140 |
-
|
| 141 |
-
---
|
| 142 |
-
|
| 143 |
-
## [1.0.0] - Initial Release
|
| 144 |
-
|
| 145 |
-
### Added
|
| 146 |
-
- Basic image validation pipeline
|
| 147 |
-
- Initial API endpoints
|
| 148 |
-
- Development server configuration
|
| 149 |
-
- Basic documentation
|
| 150 |
-
|
| 151 |
-
### Known Issues (Resolved in v2.0)
|
| 152 |
-
- Low acceptance rate (16.67%)
|
| 153 |
-
- Strict validation rules not suitable for mobile
|
| 154 |
-
- Limited documentation
|
| 155 |
-
- No weighted scoring system
|
| 156 |
-
|
| 157 |
-
---
|
| 158 |
-
|
| 159 |
-
**Version**: 2.0.0
|
| 160 |
-
**Status**: ✅ Production Ready
|
| 161 |
-
**Last Updated**: September 26, 2025
|
| 162 |
-
**Repository**: https://github.com/nitish-niraj/civic-photo-quality-control
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -53,7 +53,7 @@ COPY --from=builder /usr/local/bin /usr/local/bin
|
|
| 53 |
COPY . .
|
| 54 |
|
| 55 |
# Create necessary directories
|
| 56 |
-
RUN mkdir -p storage/temp storage/processed storage/rejected
|
| 57 |
|
| 58 |
# Set ownership and permissions
|
| 59 |
RUN chown -R app:app /app && \
|
|
@@ -62,15 +62,12 @@ RUN chown -R app:app /app && \
|
|
| 62 |
# Switch to non-root user
|
| 63 |
USER app
|
| 64 |
|
| 65 |
-
# Download YOLO model if not present
|
| 66 |
-
RUN python -c "from ultralytics import YOLO; YOLO('yolov8n.pt')" || true
|
| 67 |
-
|
| 68 |
# Expose port
|
| 69 |
EXPOSE 8000
|
| 70 |
|
| 71 |
# Health check
|
| 72 |
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 \
|
| 73 |
-
CMD
|
| 74 |
|
| 75 |
# Production server command
|
| 76 |
-
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "
|
|
|
|
| 53 |
COPY . .
|
| 54 |
|
| 55 |
# Create necessary directories
|
| 56 |
+
RUN mkdir -p storage/temp storage/processed storage/rejected
|
| 57 |
|
| 58 |
# Set ownership and permissions
|
| 59 |
RUN chown -R app:app /app && \
|
|
|
|
| 62 |
# Switch to non-root user
|
| 63 |
USER app
|
| 64 |
|
|
|
|
|
|
|
|
|
|
| 65 |
# Expose port
|
| 66 |
EXPOSE 8000
|
| 67 |
|
| 68 |
# Health check
|
| 69 |
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 \
|
| 70 |
+
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" || exit 1
|
| 71 |
|
| 72 |
# Production server command
|
| 73 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "production:app"]
|
QUICK_START.md
DELETED
|
@@ -1,271 +0,0 @@
|
|
| 1 |
-
# 🚀 Quick Production Deployment Guide
|
| 2 |
-
|
| 3 |
-
**Civic Quality Control API v2.0** - Ready for immediate production deployment!
|
| 4 |
-
|
| 5 |
-
## ⚡ 60-Second Deployment
|
| 6 |
-
|
| 7 |
-
### **1. Quick Docker Deployment**
|
| 8 |
-
```bash
|
| 9 |
-
# Clone and build
|
| 10 |
-
git clone <your-repo-url> civic_quality_app
|
| 11 |
-
cd civic_quality_app
|
| 12 |
-
|
| 13 |
-
# Set production environment
|
| 14 |
-
export SECRET_KEY="your-production-secret-key-256-bit"
|
| 15 |
-
|
| 16 |
-
# Deploy immediately
|
| 17 |
-
docker-compose up -d
|
| 18 |
-
|
| 19 |
-
# Verify deployment (should return "healthy")
|
| 20 |
-
curl http://localhost:8000/api/health
|
| 21 |
-
```
|
| 22 |
-
|
| 23 |
-
### **2. Test Your Deployment**
|
| 24 |
-
```bash
|
| 25 |
-
# Test image validation
|
| 26 |
-
curl -X POST -F 'image=@your_test_photo.jpg' \
|
| 27 |
-
http://localhost:8000/api/validate
|
| 28 |
-
|
| 29 |
-
# Check acceptance rate (should be 35-40%)
|
| 30 |
-
curl http://localhost:8000/api/summary
|
| 31 |
-
```
|
| 32 |
-
|
| 33 |
-
**✅ Production Ready!** Your API is now running at `http://localhost:8000`
|
| 34 |
-
|
| 35 |
-
---
|
| 36 |
-
|
| 37 |
-
## 🎯 What You Get Out-of-the-Box
|
| 38 |
-
|
| 39 |
-
### **✅ Mobile-Optimized Validation**
|
| 40 |
-
- **35-40% acceptance rate** for quality mobile photos
|
| 41 |
-
- **Weighted scoring system** with partial credit
|
| 42 |
-
- **<2 second processing** per image
|
| 43 |
-
- **5-component analysis**: blur, resolution, brightness, exposure, metadata
|
| 44 |
-
|
| 45 |
-
### **✅ Complete API Suite**
|
| 46 |
-
```bash
|
| 47 |
-
GET /api/health # System status
|
| 48 |
-
POST /api/validate # Image validation (primary)
|
| 49 |
-
GET /api/summary # Processing statistics
|
| 50 |
-
GET /api/validation-rules # Current thresholds
|
| 51 |
-
GET /api/test-api # API information
|
| 52 |
-
POST /api/upload # Legacy endpoint
|
| 53 |
-
```
|
| 54 |
-
|
| 55 |
-
### **✅ Production Features**
|
| 56 |
-
- **Secure file handling** (32MB limit, format validation)
|
| 57 |
-
- **Comprehensive error handling**
|
| 58 |
-
- **Automatic cleanup** of temporary files
|
| 59 |
-
- **Detailed logging** and monitoring
|
| 60 |
-
- **Mobile web interface** included
|
| 61 |
-
|
| 62 |
-
---
|
| 63 |
-
|
| 64 |
-
## 📊 Current Performance Metrics
|
| 65 |
-
|
| 66 |
-
| Metric | Value | Status |
|
| 67 |
-
|--------|-------|--------|
|
| 68 |
-
| **Acceptance Rate** | 35-40% | ✅ Optimized |
|
| 69 |
-
| **Processing Time** | <2 seconds | ✅ Fast |
|
| 70 |
-
| **API Endpoints** | 6 functional | ✅ Complete |
|
| 71 |
-
| **Mobile Support** | Full compatibility | ✅ Ready |
|
| 72 |
-
| **Error Handling** | Comprehensive | ✅ Robust |
|
| 73 |
-
|
| 74 |
-
---
|
| 75 |
-
|
| 76 |
-
## 🔧 Environment Configuration
|
| 77 |
-
|
| 78 |
-
### **Required Environment Variables**
|
| 79 |
-
```bash
|
| 80 |
-
# Minimal required setup
|
| 81 |
-
export SECRET_KEY="your-256-bit-production-secret-key"
|
| 82 |
-
export FLASK_ENV="production"
|
| 83 |
-
|
| 84 |
-
# Optional optimizations
|
| 85 |
-
export MAX_CONTENT_LENGTH="33554432" # 32MB
|
| 86 |
-
export WORKERS="4" # CPU cores
|
| 87 |
-
```
|
| 88 |
-
|
| 89 |
-
### **Optional: Custom Validation Rules**
|
| 90 |
-
The system is already optimized for mobile photography, but you can adjust in `config.py`:
|
| 91 |
-
|
| 92 |
-
```python
|
| 93 |
-
VALIDATION_RULES = {
|
| 94 |
-
"blur": {"min_score": 100}, # Laplacian variance
|
| 95 |
-
"brightness": {"range": [50, 220]}, # Pixel intensity
|
| 96 |
-
"resolution": {"min_megapixels": 0.5}, # 800x600 minimum
|
| 97 |
-
"exposure": {"min_score": 100}, # Dynamic range
|
| 98 |
-
"metadata": {"min_completeness_percentage": 15} # EXIF data
|
| 99 |
-
}
|
| 100 |
-
```
|
| 101 |
-
|
| 102 |
-
---
|
| 103 |
-
|
| 104 |
-
## 🌐 Access Your Production API
|
| 105 |
-
|
| 106 |
-
### **Primary Endpoints**
|
| 107 |
-
- **Health Check**: `http://your-domain:8000/api/health`
|
| 108 |
-
- **Image Validation**: `POST http://your-domain:8000/api/validate`
|
| 109 |
-
- **Statistics**: `http://your-domain:8000/api/summary`
|
| 110 |
-
- **Mobile Interface**: `http://your-domain:8000/mobile_upload.html`
|
| 111 |
-
|
| 112 |
-
### **Example Usage**
|
| 113 |
-
```javascript
|
| 114 |
-
// JavaScript example
|
| 115 |
-
const formData = new FormData();
|
| 116 |
-
formData.append('image', imageFile);
|
| 117 |
-
|
| 118 |
-
fetch('/api/validate', {
|
| 119 |
-
method: 'POST',
|
| 120 |
-
body: formData
|
| 121 |
-
})
|
| 122 |
-
.then(response => response.json())
|
| 123 |
-
.then(data => {
|
| 124 |
-
if (data.success && data.data.summary.overall_status === 'PASS') {
|
| 125 |
-
console.log(`Image accepted with ${data.data.summary.overall_score}% score`);
|
| 126 |
-
}
|
| 127 |
-
});
|
| 128 |
-
```
|
| 129 |
-
|
| 130 |
-
---
|
| 131 |
-
|
| 132 |
-
## 🔒 Production Security
|
| 133 |
-
|
| 134 |
-
### **✅ Security Features Included**
|
| 135 |
-
- **File type validation** (images only)
|
| 136 |
-
- **Size limits** (32MB maximum)
|
| 137 |
-
- **Input sanitization** (all uploads validated)
|
| 138 |
-
- **Temporary file cleanup** (automatic)
|
| 139 |
-
- **Environment variable secrets** (externalized)
|
| 140 |
-
- **Error message sanitization** (no sensitive data exposed)
|
| 141 |
-
|
| 142 |
-
### **Recommended Additional Security**
|
| 143 |
-
```bash
|
| 144 |
-
# Setup firewall
|
| 145 |
-
ufw allow 22 80 443 8000
|
| 146 |
-
ufw enable
|
| 147 |
-
|
| 148 |
-
# Use HTTPS in production (recommended)
|
| 149 |
-
# Configure SSL certificate
|
| 150 |
-
# Set up reverse proxy (nginx/Apache)
|
| 151 |
-
```
|
| 152 |
-
|
| 153 |
-
---
|
| 154 |
-
|
| 155 |
-
## 📈 Monitoring Your Production System
|
| 156 |
-
|
| 157 |
-
### **Health Monitoring**
|
| 158 |
-
```bash
|
| 159 |
-
# Automated health checks
|
| 160 |
-
*/5 * * * * curl -f http://your-domain:8000/api/health || alert
|
| 161 |
-
|
| 162 |
-
# Performance monitoring
|
| 163 |
-
curl -w "%{time_total}" http://your-domain:8000/api/health
|
| 164 |
-
|
| 165 |
-
# Acceptance rate tracking
|
| 166 |
-
curl http://your-domain:8000/api/summary | jq '.data.acceptance_rate'
|
| 167 |
-
```
|
| 168 |
-
|
| 169 |
-
### **Log Monitoring**
|
| 170 |
-
```bash
|
| 171 |
-
# Application logs
|
| 172 |
-
tail -f logs/app.log
|
| 173 |
-
|
| 174 |
-
# Docker logs
|
| 175 |
-
docker-compose logs -f civic-quality-app
|
| 176 |
-
|
| 177 |
-
# System resources
|
| 178 |
-
htop
|
| 179 |
-
df -h
|
| 180 |
-
```
|
| 181 |
-
|
| 182 |
-
---
|
| 183 |
-
|
| 184 |
-
## 🚨 Quick Troubleshooting
|
| 185 |
-
|
| 186 |
-
### **Common Issues & 10-Second Fixes**
|
| 187 |
-
|
| 188 |
-
#### **API Not Responding**
|
| 189 |
-
```bash
|
| 190 |
-
curl http://localhost:8000/api/health
|
| 191 |
-
# If no response: docker-compose restart civic-quality-app
|
| 192 |
-
```
|
| 193 |
-
|
| 194 |
-
#### **Low Acceptance Rate**
|
| 195 |
-
```bash
|
| 196 |
-
# Check current rate
|
| 197 |
-
curl http://localhost:8000/api/summary
|
| 198 |
-
# System already optimized to 35-40% - this is correct for mobile photos
|
| 199 |
-
```
|
| 200 |
-
|
| 201 |
-
#### **Slow Processing**
|
| 202 |
-
```bash
|
| 203 |
-
# Check processing time
|
| 204 |
-
time curl -X POST -F 'image=@test.jpg' http://localhost:8000/api/validate
|
| 205 |
-
# If >3 seconds: increase worker count or check system resources
|
| 206 |
-
```
|
| 207 |
-
|
| 208 |
-
#### **Storage Issues**
|
| 209 |
-
```bash
|
| 210 |
-
df -h # Check disk space
|
| 211 |
-
# Clean temp files: find storage/temp -type f -mtime +1 -delete
|
| 212 |
-
```
|
| 213 |
-
|
| 214 |
-
---
|
| 215 |
-
|
| 216 |
-
## 📋 Production Deployment Variants
|
| 217 |
-
|
| 218 |
-
### **Variant 1: Single Server**
|
| 219 |
-
```bash
|
| 220 |
-
# Simple single-server deployment
|
| 221 |
-
docker run -d --name civic-quality \
|
| 222 |
-
-p 8000:8000 \
|
| 223 |
-
-e SECRET_KEY="your-key" \
|
| 224 |
-
civic-quality-app:v2.0
|
| 225 |
-
```
|
| 226 |
-
|
| 227 |
-
### **Variant 2: Load Balanced**
|
| 228 |
-
```bash
|
| 229 |
-
# Multiple instances with load balancer
|
| 230 |
-
docker run -d --name civic-quality-1 -p 8001:8000 civic-quality-app:v2.0
|
| 231 |
-
docker run -d --name civic-quality-2 -p 8002:8000 civic-quality-app:v2.0
|
| 232 |
-
# Configure nginx/ALB to distribute traffic
|
| 233 |
-
```
|
| 234 |
-
|
| 235 |
-
### **Variant 3: Cloud Deployment**
|
| 236 |
-
```bash
|
| 237 |
-
# AWS/Azure/GCP
|
| 238 |
-
# Use production Docker image: civic-quality-app:v2.0
|
| 239 |
-
# Set environment variables via cloud console
|
| 240 |
-
# Configure auto-scaling and load balancing
|
| 241 |
-
```
|
| 242 |
-
|
| 243 |
-
---
|
| 244 |
-
|
| 245 |
-
## 🎉 You're Production Ready!
|
| 246 |
-
|
| 247 |
-
**Congratulations!** Your Civic Quality Control API v2.0 is now:
|
| 248 |
-
|
| 249 |
-
✅ **Deployed and running**
|
| 250 |
-
✅ **Mobile-optimized** (35-40% acceptance rate)
|
| 251 |
-
✅ **High-performance** (<2 second processing)
|
| 252 |
-
✅ **Fully documented** (API docs included)
|
| 253 |
-
✅ **Production-hardened** (security & monitoring)
|
| 254 |
-
|
| 255 |
-
### **What's Next?**
|
| 256 |
-
1. **Point your mobile app** to the API endpoints
|
| 257 |
-
2. **Set up monitoring alerts** for health and performance
|
| 258 |
-
3. **Configure HTTPS** for production security
|
| 259 |
-
4. **Scale as needed** based on usage patterns
|
| 260 |
-
|
| 261 |
-
### **Support Resources**
|
| 262 |
-
- **Full Documentation**: `docs/README.md`, `docs/API_v2.md`, `docs/DEPLOYMENT.md`
|
| 263 |
-
- **Test Your API**: Run `python api_test.py`
|
| 264 |
-
- **Mobile Interface**: Access at `/mobile_upload.html`
|
| 265 |
-
- **Configuration**: Adjust rules in `config.py` if needed
|
| 266 |
-
|
| 267 |
-
---
|
| 268 |
-
|
| 269 |
-
**Quick Start Guide Version**: 2.0
|
| 270 |
-
**Deployment Status**: ✅ **PRODUCTION READY**
|
| 271 |
-
**Updated**: September 25, 2025
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
## 🚀 Key Features
|
| 6 |
|
|
@@ -49,18 +49,20 @@ pip install -r requirements.txt
|
|
| 49 |
### Setup & Run
|
| 50 |
|
| 51 |
```bash
|
| 52 |
-
#
|
| 53 |
-
python scripts/setup_directories.py
|
| 54 |
-
python scripts/download_models.py
|
| 55 |
-
|
| 56 |
-
# Start development server
|
| 57 |
python app.py
|
| 58 |
|
| 59 |
-
# Access
|
| 60 |
-
# http://localhost:5000
|
| 61 |
```
|
| 62 |
|
| 63 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
### Core Endpoints
|
| 66 |
|
|
@@ -140,17 +142,7 @@ GET /api/validation-rules
|
|
| 140 |
```
|
| 141 |
Returns current validation thresholds and requirements.
|
| 142 |
|
| 143 |
-
### Testing Endpoints
|
| 144 |
-
|
| 145 |
-
#### 5. API Information
|
| 146 |
-
```bash
|
| 147 |
-
GET /api/test-api
|
| 148 |
-
```
|
| 149 |
|
| 150 |
-
#### 6. Legacy Upload (Deprecated)
|
| 151 |
-
```bash
|
| 152 |
-
POST /api/upload
|
| 153 |
-
```
|
| 154 |
|
| 155 |
## 🏗️ Production Deployment
|
| 156 |
|
|
@@ -266,8 +258,7 @@ civic_quality_app/
|
|
| 266 |
│ ├── brightness_validation.py
|
| 267 |
│ ├── exposure_check.py
|
| 268 |
│ ├── resolution_check.py
|
| 269 |
-
│
|
| 270 |
-
│ └── object_detection.py
|
| 271 |
│
|
| 272 |
├── storage/ # File storage
|
| 273 |
│ ├── temp/ # Temporary uploads
|
|
@@ -366,14 +357,12 @@ For issues and improvements:
|
|
| 366 |
### Future Enhancements
|
| 367 |
|
| 368 |
- [ ] Real-time processing optimization
|
| 369 |
-
- [ ] Advanced object detection integration
|
| 370 |
-
- [ ] GPS metadata validation
|
| 371 |
- [ ] Batch processing capabilities
|
| 372 |
- [ ] API rate limiting
|
| 373 |
- [ ] Enhanced mobile UI
|
| 374 |
|
| 375 |
---
|
| 376 |
|
| 377 |
-
**Version**:
|
| 378 |
-
**Last Updated**:
|
| 379 |
**Production Status**: ✅ Ready for deployment
|
|
|
|
| 1 |
+
# PhotoGuard API
|
| 2 |
|
| 3 |
+
Professional image quality validation system with automated blur detection, brightness analysis, resolution checking, exposure verification, and metadata extraction.
|
| 4 |
|
| 5 |
## 🚀 Key Features
|
| 6 |
|
|
|
|
| 49 |
### Setup & Run
|
| 50 |
|
| 51 |
```bash
|
| 52 |
+
# Start development server (creates storage directories automatically)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
python app.py
|
| 54 |
|
| 55 |
+
# Access the web interface
|
| 56 |
+
# http://localhost:5000
|
| 57 |
```
|
| 58 |
|
| 59 |
+
## � API Documentation
|
| 60 |
+
|
| 61 |
+
### Interactive Documentation
|
| 62 |
+
|
| 63 |
+
- **Swagger UI**: http://localhost:5000/api/docs - Interactive API testing interface
|
| 64 |
+
- **ReDoc**: http://localhost:5000/api/redoc - Comprehensive API reference
|
| 65 |
+
- **OpenAPI Spec**: http://localhost:5000/api/openapi.json - OpenAPI 3.1 specification
|
| 66 |
|
| 67 |
### Core Endpoints
|
| 68 |
|
|
|
|
| 142 |
```
|
| 143 |
Returns current validation thresholds and requirements.
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
## 🏗️ Production Deployment
|
| 148 |
|
|
|
|
| 258 |
│ ├── brightness_validation.py
|
| 259 |
│ ├── exposure_check.py
|
| 260 |
│ ├── resolution_check.py
|
| 261 |
+
│ └── metadata_extraction.py
|
|
|
|
| 262 |
│
|
| 263 |
├── storage/ # File storage
|
| 264 |
│ ├── temp/ # Temporary uploads
|
|
|
|
| 357 |
### Future Enhancements
|
| 358 |
|
| 359 |
- [ ] Real-time processing optimization
|
|
|
|
|
|
|
| 360 |
- [ ] Batch processing capabilities
|
| 361 |
- [ ] API rate limiting
|
| 362 |
- [ ] Enhanced mobile UI
|
| 363 |
|
| 364 |
---
|
| 365 |
|
| 366 |
+
**Version**: 3.0.0
|
| 367 |
+
**Last Updated**: November 3, 2025
|
| 368 |
**Production Status**: ✅ Ready for deployment
|
api_test.py
DELETED
|
@@ -1,187 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
API Test Script for Civic Quality Control App
|
| 4 |
-
Demonstrates how to test the updated validation API endpoints.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import requests
|
| 8 |
-
import json
|
| 9 |
-
import os
|
| 10 |
-
import sys
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
|
| 13 |
-
# Configuration
|
| 14 |
-
API_BASE_URL = "http://localhost:5000/api"
|
| 15 |
-
TEST_IMAGE_PATH = "storage/temp/7db56d0e-ff94-49ca-b61a-5f33469fe4af_IMG_20220629_174412.jpg"
|
| 16 |
-
|
| 17 |
-
def test_health_endpoint():
|
| 18 |
-
"""Test the health check endpoint."""
|
| 19 |
-
print("🔍 Testing Health Endpoint...")
|
| 20 |
-
try:
|
| 21 |
-
response = requests.get(f"{API_BASE_URL}/health")
|
| 22 |
-
print(f"Status Code: {response.status_code}")
|
| 23 |
-
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
| 24 |
-
return response.status_code == 200
|
| 25 |
-
except Exception as e:
|
| 26 |
-
print(f"❌ Health check failed: {e}")
|
| 27 |
-
return False
|
| 28 |
-
|
| 29 |
-
def test_validation_rules_endpoint():
|
| 30 |
-
"""Test the validation rules endpoint."""
|
| 31 |
-
print("\n🔍 Testing Validation Rules Endpoint...")
|
| 32 |
-
try:
|
| 33 |
-
response = requests.get(f"{API_BASE_URL}/validation-rules")
|
| 34 |
-
print(f"Status Code: {response.status_code}")
|
| 35 |
-
if response.status_code == 200:
|
| 36 |
-
rules = response.json()
|
| 37 |
-
print("✅ Validation Rules Retrieved:")
|
| 38 |
-
print(json.dumps(rules, indent=2))
|
| 39 |
-
return response.status_code == 200
|
| 40 |
-
except Exception as e:
|
| 41 |
-
print(f"❌ Validation rules test failed: {e}")
|
| 42 |
-
return False
|
| 43 |
-
|
| 44 |
-
def test_api_info_endpoint():
|
| 45 |
-
"""Test the API information endpoint."""
|
| 46 |
-
print("\n🔍 Testing API Information Endpoint...")
|
| 47 |
-
try:
|
| 48 |
-
response = requests.get(f"{API_BASE_URL}/test-api")
|
| 49 |
-
print(f"Status Code: {response.status_code}")
|
| 50 |
-
if response.status_code == 200:
|
| 51 |
-
info = response.json()
|
| 52 |
-
print("✅ API Information Retrieved:")
|
| 53 |
-
print(f"API Version: {info['data']['api_version']}")
|
| 54 |
-
print(f"Available Endpoints: {len(info['data']['endpoints'])}")
|
| 55 |
-
print("\nEndpoints:")
|
| 56 |
-
for endpoint, description in info['data']['endpoints'].items():
|
| 57 |
-
print(f" {endpoint}: {description}")
|
| 58 |
-
return response.status_code == 200
|
| 59 |
-
except Exception as e:
|
| 60 |
-
print(f"❌ API info test failed: {e}")
|
| 61 |
-
return False
|
| 62 |
-
|
| 63 |
-
def test_image_validation_endpoint():
|
| 64 |
-
"""Test the main image validation endpoint."""
|
| 65 |
-
print("\n🔍 Testing Image Validation Endpoint...")
|
| 66 |
-
|
| 67 |
-
# Check if test image exists
|
| 68 |
-
if not os.path.exists(TEST_IMAGE_PATH):
|
| 69 |
-
print(f"❌ Test image not found: {TEST_IMAGE_PATH}")
|
| 70 |
-
print("Please ensure you have an image in the storage/temp folder or update TEST_IMAGE_PATH")
|
| 71 |
-
return False
|
| 72 |
-
|
| 73 |
-
try:
|
| 74 |
-
# Prepare file for upload
|
| 75 |
-
with open(TEST_IMAGE_PATH, 'rb') as f:
|
| 76 |
-
files = {'image': f}
|
| 77 |
-
response = requests.post(f"{API_BASE_URL}/validate", files=files)
|
| 78 |
-
|
| 79 |
-
print(f"Status Code: {response.status_code}")
|
| 80 |
-
|
| 81 |
-
if response.status_code == 200:
|
| 82 |
-
result = response.json()
|
| 83 |
-
print("✅ Image Validation Completed!")
|
| 84 |
-
|
| 85 |
-
# Extract key information
|
| 86 |
-
data = result['data']
|
| 87 |
-
summary = data['summary']
|
| 88 |
-
checks = data['checks']
|
| 89 |
-
|
| 90 |
-
print(f"\n📊 Overall Status: {summary['overall_status'].upper()}")
|
| 91 |
-
print(f"📊 Overall Score: {summary['overall_score']}")
|
| 92 |
-
print(f"📊 Issues Found: {summary['issues_found']}")
|
| 93 |
-
|
| 94 |
-
# Show validation results
|
| 95 |
-
print("\n📋 Validation Results:")
|
| 96 |
-
for check_type, check_result in checks.items():
|
| 97 |
-
if check_result:
|
| 98 |
-
status = "✅ PASS" if check_result.get('status') == 'pass' else "❌ FAIL"
|
| 99 |
-
reason = check_result.get('reason', 'unknown')
|
| 100 |
-
print(f" {check_type}: {status} - {reason}")
|
| 101 |
-
|
| 102 |
-
# Show recommendations if any
|
| 103 |
-
if summary['recommendations']:
|
| 104 |
-
print(f"\n💡 Recommendations ({len(summary['recommendations'])}):")
|
| 105 |
-
for rec in summary['recommendations']:
|
| 106 |
-
print(f" - {rec}")
|
| 107 |
-
|
| 108 |
-
else:
|
| 109 |
-
print(f"❌ Validation failed with status {response.status_code}")
|
| 110 |
-
print(f"Response: {response.text}")
|
| 111 |
-
|
| 112 |
-
return response.status_code == 200
|
| 113 |
-
|
| 114 |
-
except Exception as e:
|
| 115 |
-
print(f"❌ Image validation test failed: {e}")
|
| 116 |
-
return False
|
| 117 |
-
|
| 118 |
-
def test_summary_endpoint():
|
| 119 |
-
"""Test the processing summary endpoint."""
|
| 120 |
-
print("\n🔍 Testing Summary Endpoint...")
|
| 121 |
-
try:
|
| 122 |
-
response = requests.get(f"{API_BASE_URL}/summary")
|
| 123 |
-
print(f"Status Code: {response.status_code}")
|
| 124 |
-
if response.status_code == 200:
|
| 125 |
-
summary = response.json()
|
| 126 |
-
print("✅ Processing Summary Retrieved:")
|
| 127 |
-
data = summary['data']
|
| 128 |
-
print(f" Total Images Processed: {data.get('total_images', 0)}")
|
| 129 |
-
print(f" Accepted Images: {data.get('total_processed', 0)}")
|
| 130 |
-
print(f" Rejected Images: {data.get('total_rejected', 0)}")
|
| 131 |
-
print(f" Acceptance Rate: {data.get('acceptance_rate', 0)}%")
|
| 132 |
-
return response.status_code == 200
|
| 133 |
-
except Exception as e:
|
| 134 |
-
print(f"❌ Summary test failed: {e}")
|
| 135 |
-
return False
|
| 136 |
-
|
| 137 |
-
def main():
|
| 138 |
-
"""Run all API tests."""
|
| 139 |
-
print("🚀 Starting Civic Quality Control API Tests")
|
| 140 |
-
print("=" * 50)
|
| 141 |
-
|
| 142 |
-
# Check if server is running
|
| 143 |
-
try:
|
| 144 |
-
requests.get(API_BASE_URL, timeout=5)
|
| 145 |
-
except requests.exceptions.ConnectionError:
|
| 146 |
-
print("❌ Server not running! Please start the server first:")
|
| 147 |
-
print(" python app.py")
|
| 148 |
-
print(" Or: python production.py")
|
| 149 |
-
sys.exit(1)
|
| 150 |
-
|
| 151 |
-
# Run tests
|
| 152 |
-
tests = [
|
| 153 |
-
("Health Check", test_health_endpoint),
|
| 154 |
-
("Validation Rules", test_validation_rules_endpoint),
|
| 155 |
-
("API Information", test_api_info_endpoint),
|
| 156 |
-
("Image Validation", test_image_validation_endpoint),
|
| 157 |
-
("Processing Summary", test_summary_endpoint),
|
| 158 |
-
]
|
| 159 |
-
|
| 160 |
-
passed = 0
|
| 161 |
-
total = len(tests)
|
| 162 |
-
|
| 163 |
-
for test_name, test_func in tests:
|
| 164 |
-
print(f"\n{'='*20} {test_name} {'='*20}")
|
| 165 |
-
if test_func():
|
| 166 |
-
passed += 1
|
| 167 |
-
print(f"✅ {test_name} PASSED")
|
| 168 |
-
else:
|
| 169 |
-
print(f"❌ {test_name} FAILED")
|
| 170 |
-
|
| 171 |
-
# Final results
|
| 172 |
-
print("\n" + "="*50)
|
| 173 |
-
print(f"📊 Test Results: {passed}/{total} tests passed")
|
| 174 |
-
|
| 175 |
-
if passed == total:
|
| 176 |
-
print("🎉 All tests passed! API is working correctly.")
|
| 177 |
-
else:
|
| 178 |
-
print("⚠️ Some tests failed. Check the output above for details.")
|
| 179 |
-
|
| 180 |
-
print("\n💡 API Usage Examples:")
|
| 181 |
-
print(f" Health Check: curl {API_BASE_URL}/health")
|
| 182 |
-
print(f" Get Rules: curl {API_BASE_URL}/validation-rules")
|
| 183 |
-
print(f" Validate Image: curl -X POST -F 'image=@your_image.jpg' {API_BASE_URL}/validate")
|
| 184 |
-
print(f" Get Summary: curl {API_BASE_URL}/summary")
|
| 185 |
-
|
| 186 |
-
if __name__ == "__main__":
|
| 187 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/__init__.py
CHANGED
|
@@ -1,40 +1,47 @@
|
|
| 1 |
-
from flask import Flask
|
| 2 |
import os
|
| 3 |
|
| 4 |
-
def create_app(config_name='default'):
|
| 5 |
-
# Set template and static folders relative to project root
|
| 6 |
-
app = Flask(__name__,
|
| 7 |
-
template_folder='../templates',
|
| 8 |
-
static_folder='../static')
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
app
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
try:
|
| 16 |
-
from flask_cors import CORS
|
| 17 |
CORS(app)
|
| 18 |
except ImportError:
|
| 19 |
print("Warning: Flask-CORS not installed, CORS disabled")
|
| 20 |
|
| 21 |
-
#
|
| 22 |
directories = [
|
| 23 |
app.config['UPLOAD_FOLDER'],
|
| 24 |
-
'storage/processed',
|
| 25 |
-
'storage/rejected'
|
| 26 |
-
'models'
|
| 27 |
]
|
| 28 |
|
| 29 |
for directory in directories:
|
| 30 |
os.makedirs(directory, exist_ok=True)
|
| 31 |
-
# Create .gitkeep files
|
| 32 |
gitkeep_path = os.path.join(directory, '.gitkeep')
|
| 33 |
if not os.path.exists(gitkeep_path):
|
| 34 |
open(gitkeep_path, 'a').close()
|
| 35 |
|
| 36 |
-
# Register
|
| 37 |
from app.routes.upload import upload_bp
|
| 38 |
app.register_blueprint(upload_bp, url_prefix='/api')
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
return app
|
|
|
|
| 1 |
+
from flask import Flask, render_template
|
| 2 |
import os
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
def create_app(config_name: str = 'default') -> Flask:
|
| 6 |
+
"""Application factory that wires configuration, blueprints, and assets."""
|
| 7 |
+
app = Flask(
|
| 8 |
+
__name__,
|
| 9 |
+
template_folder='../templates',
|
| 10 |
+
static_folder='../static'
|
| 11 |
+
)
|
| 12 |
|
| 13 |
+
# Load configuration class by name with graceful fallback to default.
|
| 14 |
+
from config import config as config_map
|
| 15 |
+
config_class = config_map.get(config_name, config_map['default'])
|
| 16 |
+
app.config.from_object(config_class)
|
| 17 |
+
|
| 18 |
+
# Enable CORS when the optional dependency is available.
|
| 19 |
try:
|
| 20 |
+
from flask_cors import CORS # type: ignore
|
| 21 |
CORS(app)
|
| 22 |
except ImportError:
|
| 23 |
print("Warning: Flask-CORS not installed, CORS disabled")
|
| 24 |
|
| 25 |
+
# Ensure required storage directories exist so uploads succeed at runtime.
|
| 26 |
directories = [
|
| 27 |
app.config['UPLOAD_FOLDER'],
|
| 28 |
+
app.config.get('PROCESSED_FOLDER', 'storage/processed'),
|
| 29 |
+
app.config.get('REJECTED_FOLDER', 'storage/rejected')
|
|
|
|
| 30 |
]
|
| 31 |
|
| 32 |
for directory in directories:
|
| 33 |
os.makedirs(directory, exist_ok=True)
|
|
|
|
| 34 |
gitkeep_path = os.path.join(directory, '.gitkeep')
|
| 35 |
if not os.path.exists(gitkeep_path):
|
| 36 |
open(gitkeep_path, 'a').close()
|
| 37 |
|
| 38 |
+
# Register API blueprint.
|
| 39 |
from app.routes.upload import upload_bp
|
| 40 |
app.register_blueprint(upload_bp, url_prefix='/api')
|
| 41 |
|
| 42 |
+
@app.route('/')
|
| 43 |
+
def index():
|
| 44 |
+
"""Serve the main quality control interface."""
|
| 45 |
+
return render_template('index.html')
|
| 46 |
+
|
| 47 |
return app
|
app/api_spec.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PhotoGuard API - OpenAPI 3.1 Specification
|
| 3 |
+
===========================================
|
| 4 |
+
Complete API specification for PhotoGuard image quality validation system.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
OPENAPI_SPEC = {
|
| 8 |
+
"openapi": "3.1.0",
|
| 9 |
+
"info": {
|
| 10 |
+
"title": "PhotoGuard API",
|
| 11 |
+
"version": "3.0.0",
|
| 12 |
+
"description": "Professional image quality validation system with automated blur detection, brightness analysis, resolution checking, exposure verification, and metadata extraction",
|
| 13 |
+
"contact": {
|
| 14 |
+
"name": "PhotoGuard API Support"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"servers": [
|
| 18 |
+
{
|
| 19 |
+
"url": "/api",
|
| 20 |
+
"description": "API Server"
|
| 21 |
+
}
|
| 22 |
+
],
|
| 23 |
+
"tags": [
|
| 24 |
+
{
|
| 25 |
+
"name": "Image Validation",
|
| 26 |
+
"description": "Core image quality validation endpoints"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"name": "System Information",
|
| 30 |
+
"description": "System status and configuration endpoints"
|
| 31 |
+
}
|
| 32 |
+
],
|
| 33 |
+
"paths": {
|
| 34 |
+
"/validate": {
|
| 35 |
+
"post": {
|
| 36 |
+
"tags": ["Image Validation"],
|
| 37 |
+
"summary": "Validate Image Quality",
|
| 38 |
+
"description": "Upload an image and receive comprehensive quality validation results including blur detection, brightness analysis, resolution check, exposure verification, and metadata extraction",
|
| 39 |
+
"operationId": "validate_image",
|
| 40 |
+
"requestBody": {
|
| 41 |
+
"required": True,
|
| 42 |
+
"content": {
|
| 43 |
+
"multipart/form-data": {
|
| 44 |
+
"schema": {
|
| 45 |
+
"$ref": "#/components/schemas/ImageUpload"
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"responses": {
|
| 51 |
+
"200": {
|
| 52 |
+
"description": "Validation completed successfully",
|
| 53 |
+
"content": {
|
| 54 |
+
"application/json": {
|
| 55 |
+
"schema": {
|
| 56 |
+
"$ref": "#/components/schemas/ValidationResponse"
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"400": {
|
| 62 |
+
"description": "Bad request - Invalid file or missing parameters",
|
| 63 |
+
"content": {
|
| 64 |
+
"application/json": {
|
| 65 |
+
"schema": {
|
| 66 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
},
|
| 71 |
+
"413": {
|
| 72 |
+
"description": "File too large - Maximum 16MB",
|
| 73 |
+
"content": {
|
| 74 |
+
"application/json": {
|
| 75 |
+
"schema": {
|
| 76 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
},
|
| 81 |
+
"500": {
|
| 82 |
+
"description": "Internal server error",
|
| 83 |
+
"content": {
|
| 84 |
+
"application/json": {
|
| 85 |
+
"schema": {
|
| 86 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
},
|
| 94 |
+
"/validation-rules": {
|
| 95 |
+
"get": {
|
| 96 |
+
"tags": ["System Information"],
|
| 97 |
+
"summary": "Get Validation Rules",
|
| 98 |
+
"description": "Retrieve the current validation rules and thresholds used by the system",
|
| 99 |
+
"operationId": "get_validation_rules",
|
| 100 |
+
"responses": {
|
| 101 |
+
"200": {
|
| 102 |
+
"description": "Validation rules retrieved successfully",
|
| 103 |
+
"content": {
|
| 104 |
+
"application/json": {
|
| 105 |
+
"schema": {
|
| 106 |
+
"$ref": "#/components/schemas/ValidationRulesResponse"
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
},
|
| 114 |
+
"/summary": {
|
| 115 |
+
"get": {
|
| 116 |
+
"tags": ["System Information"],
|
| 117 |
+
"summary": "Get Processing Summary",
|
| 118 |
+
"description": "Retrieve aggregate validation statistics including total images processed, pass/fail counts, and average scores",
|
| 119 |
+
"operationId": "get_summary",
|
| 120 |
+
"responses": {
|
| 121 |
+
"200": {
|
| 122 |
+
"description": "Processing summary retrieved successfully",
|
| 123 |
+
"content": {
|
| 124 |
+
"application/json": {
|
| 125 |
+
"schema": {
|
| 126 |
+
"$ref": "#/components/schemas/SummaryResponse"
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
},
|
| 131 |
+
"500": {
|
| 132 |
+
"description": "Internal server error",
|
| 133 |
+
"content": {
|
| 134 |
+
"application/json": {
|
| 135 |
+
"schema": {
|
| 136 |
+
"$ref": "#/components/schemas/ErrorResponse"
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
},
|
| 144 |
+
"/health": {
|
| 145 |
+
"get": {
|
| 146 |
+
"tags": ["System Information"],
|
| 147 |
+
"summary": "Health Check",
|
| 148 |
+
"description": "Check if the API service is running and healthy",
|
| 149 |
+
"operationId": "health_check",
|
| 150 |
+
"responses": {
|
| 151 |
+
"200": {
|
| 152 |
+
"description": "Service is healthy",
|
| 153 |
+
"content": {
|
| 154 |
+
"application/json": {
|
| 155 |
+
"schema": {
|
| 156 |
+
"$ref": "#/components/schemas/HealthResponse"
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
},
|
| 165 |
+
"components": {
|
| 166 |
+
"schemas": {
|
| 167 |
+
"ImageUpload": {
|
| 168 |
+
"type": "object",
|
| 169 |
+
"required": ["image"],
|
| 170 |
+
"properties": {
|
| 171 |
+
"image": {
|
| 172 |
+
"type": "string",
|
| 173 |
+
"format": "binary",
|
| 174 |
+
"description": "Image file to validate (jpg, jpeg, png, bmp, tiff). Maximum size: 16MB"
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
},
|
| 178 |
+
"ValidationResponse": {
|
| 179 |
+
"type": "object",
|
| 180 |
+
"properties": {
|
| 181 |
+
"success": {
|
| 182 |
+
"type": "boolean",
|
| 183 |
+
"description": "Whether the request was processed successfully",
|
| 184 |
+
"example": True
|
| 185 |
+
},
|
| 186 |
+
"message": {
|
| 187 |
+
"type": "string",
|
| 188 |
+
"description": "Response message",
|
| 189 |
+
"example": "Image validation completed"
|
| 190 |
+
},
|
| 191 |
+
"data": {
|
| 192 |
+
"type": "object",
|
| 193 |
+
"properties": {
|
| 194 |
+
"summary": {
|
| 195 |
+
"type": "object",
|
| 196 |
+
"properties": {
|
| 197 |
+
"overall_status": {
|
| 198 |
+
"type": "string",
|
| 199 |
+
"enum": ["pass", "fail"],
|
| 200 |
+
"description": "Overall validation result"
|
| 201 |
+
},
|
| 202 |
+
"overall_score": {
|
| 203 |
+
"type": "number",
|
| 204 |
+
"format": "float",
|
| 205 |
+
"description": "Overall weighted quality score (0-100)",
|
| 206 |
+
"example": 78.5
|
| 207 |
+
},
|
| 208 |
+
"issues_found": {
|
| 209 |
+
"type": "array",
|
| 210 |
+
"items": {
|
| 211 |
+
"type": "string"
|
| 212 |
+
},
|
| 213 |
+
"description": "List of quality issues detected"
|
| 214 |
+
},
|
| 215 |
+
"recommendations": {
|
| 216 |
+
"type": "array",
|
| 217 |
+
"items": {
|
| 218 |
+
"type": "string"
|
| 219 |
+
},
|
| 220 |
+
"description": "Recommendations for improving image quality"
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
},
|
| 224 |
+
"checks": {
|
| 225 |
+
"type": "object",
|
| 226 |
+
"properties": {
|
| 227 |
+
"blur": {
|
| 228 |
+
"$ref": "#/components/schemas/BlurCheck"
|
| 229 |
+
},
|
| 230 |
+
"brightness": {
|
| 231 |
+
"$ref": "#/components/schemas/BrightnessCheck"
|
| 232 |
+
},
|
| 233 |
+
"resolution": {
|
| 234 |
+
"$ref": "#/components/schemas/ResolutionCheck"
|
| 235 |
+
},
|
| 236 |
+
"exposure": {
|
| 237 |
+
"$ref": "#/components/schemas/ExposureCheck"
|
| 238 |
+
},
|
| 239 |
+
"metadata": {
|
| 240 |
+
"$ref": "#/components/schemas/MetadataCheck"
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
},
|
| 248 |
+
"BlurCheck": {
|
| 249 |
+
"type": "object",
|
| 250 |
+
"properties": {
|
| 251 |
+
"status": {
|
| 252 |
+
"type": "string",
|
| 253 |
+
"enum": ["pass", "fail"],
|
| 254 |
+
"description": "Blur check result"
|
| 255 |
+
},
|
| 256 |
+
"score": {
|
| 257 |
+
"type": "number",
|
| 258 |
+
"format": "float",
|
| 259 |
+
"description": "Laplacian variance score (higher = sharper)",
|
| 260 |
+
"example": 245.8
|
| 261 |
+
},
|
| 262 |
+
"threshold": {
|
| 263 |
+
"type": "number",
|
| 264 |
+
"format": "float",
|
| 265 |
+
"description": "Minimum acceptable blur score",
|
| 266 |
+
"example": 100.0
|
| 267 |
+
},
|
| 268 |
+
"quality_percentage": {
|
| 269 |
+
"type": "number",
|
| 270 |
+
"format": "float",
|
| 271 |
+
"description": "Quality score as percentage (0-100)",
|
| 272 |
+
"example": 85.5
|
| 273 |
+
}
|
| 274 |
+
}
|
| 275 |
+
},
|
| 276 |
+
"BrightnessCheck": {
|
| 277 |
+
"type": "object",
|
| 278 |
+
"properties": {
|
| 279 |
+
"status": {
|
| 280 |
+
"type": "string",
|
| 281 |
+
"enum": ["pass", "fail"],
|
| 282 |
+
"description": "Brightness check result"
|
| 283 |
+
},
|
| 284 |
+
"mean_brightness": {
|
| 285 |
+
"type": "number",
|
| 286 |
+
"format": "float",
|
| 287 |
+
"description": "Mean pixel intensity (0-255)",
|
| 288 |
+
"example": 145.3
|
| 289 |
+
},
|
| 290 |
+
"acceptable_range": {
|
| 291 |
+
"type": "array",
|
| 292 |
+
"items": {
|
| 293 |
+
"type": "integer"
|
| 294 |
+
},
|
| 295 |
+
"description": "Acceptable brightness range",
|
| 296 |
+
"example": [50, 220]
|
| 297 |
+
},
|
| 298 |
+
"quality_percentage": {
|
| 299 |
+
"type": "number",
|
| 300 |
+
"format": "float",
|
| 301 |
+
"description": "Quality score as percentage (0-100)",
|
| 302 |
+
"example": 92.0
|
| 303 |
+
}
|
| 304 |
+
}
|
| 305 |
+
},
|
| 306 |
+
"ResolutionCheck": {
|
| 307 |
+
"type": "object",
|
| 308 |
+
"properties": {
|
| 309 |
+
"status": {
|
| 310 |
+
"type": "string",
|
| 311 |
+
"enum": ["pass", "fail"],
|
| 312 |
+
"description": "Resolution check result"
|
| 313 |
+
},
|
| 314 |
+
"width": {
|
| 315 |
+
"type": "integer",
|
| 316 |
+
"description": "Image width in pixels",
|
| 317 |
+
"example": 1920
|
| 318 |
+
},
|
| 319 |
+
"height": {
|
| 320 |
+
"type": "integer",
|
| 321 |
+
"description": "Image height in pixels",
|
| 322 |
+
"example": 1080
|
| 323 |
+
},
|
| 324 |
+
"megapixels": {
|
| 325 |
+
"type": "number",
|
| 326 |
+
"format": "float",
|
| 327 |
+
"description": "Total megapixels",
|
| 328 |
+
"example": 2.07
|
| 329 |
+
},
|
| 330 |
+
"quality_percentage": {
|
| 331 |
+
"type": "number",
|
| 332 |
+
"format": "float",
|
| 333 |
+
"description": "Quality score as percentage (0-100)",
|
| 334 |
+
"example": 100.0
|
| 335 |
+
}
|
| 336 |
+
}
|
| 337 |
+
},
|
| 338 |
+
"ExposureCheck": {
|
| 339 |
+
"type": "object",
|
| 340 |
+
"properties": {
|
| 341 |
+
"status": {
|
| 342 |
+
"type": "string",
|
| 343 |
+
"enum": ["pass", "fail"],
|
| 344 |
+
"description": "Exposure check result"
|
| 345 |
+
},
|
| 346 |
+
"dynamic_range": {
|
| 347 |
+
"type": "number",
|
| 348 |
+
"format": "float",
|
| 349 |
+
"description": "Dynamic range (difference between max and min pixel values)",
|
| 350 |
+
"example": 185.5
|
| 351 |
+
},
|
| 352 |
+
"clipping_percentage": {
|
| 353 |
+
"type": "number",
|
| 354 |
+
"format": "float",
|
| 355 |
+
"description": "Percentage of clipped pixels (pure white or black)",
|
| 356 |
+
"example": 0.5
|
| 357 |
+
},
|
| 358 |
+
"quality_percentage": {
|
| 359 |
+
"type": "number",
|
| 360 |
+
"format": "float",
|
| 361 |
+
"description": "Quality score as percentage (0-100)",
|
| 362 |
+
"example": 88.0
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
},
|
| 366 |
+
"MetadataCheck": {
|
| 367 |
+
"type": "object",
|
| 368 |
+
"properties": {
|
| 369 |
+
"status": {
|
| 370 |
+
"type": "string",
|
| 371 |
+
"enum": ["pass", "fail"],
|
| 372 |
+
"description": "Metadata check result"
|
| 373 |
+
},
|
| 374 |
+
"completeness": {
|
| 375 |
+
"type": "number",
|
| 376 |
+
"format": "float",
|
| 377 |
+
"description": "Metadata completeness percentage",
|
| 378 |
+
"example": 66.7
|
| 379 |
+
},
|
| 380 |
+
"fields_found": {
|
| 381 |
+
"type": "integer",
|
| 382 |
+
"description": "Number of metadata fields found",
|
| 383 |
+
"example": 4
|
| 384 |
+
},
|
| 385 |
+
"fields_required": {
|
| 386 |
+
"type": "integer",
|
| 387 |
+
"description": "Total number of expected metadata fields",
|
| 388 |
+
"example": 6
|
| 389 |
+
},
|
| 390 |
+
"extracted_data": {
|
| 391 |
+
"type": "object",
|
| 392 |
+
"description": "Extracted EXIF metadata",
|
| 393 |
+
"properties": {
|
| 394 |
+
"timestamp": {
|
| 395 |
+
"type": "string",
|
| 396 |
+
"description": "Image capture timestamp"
|
| 397 |
+
},
|
| 398 |
+
"camera_make_model": {
|
| 399 |
+
"type": "string",
|
| 400 |
+
"description": "Camera make and model"
|
| 401 |
+
},
|
| 402 |
+
"gps": {
|
| 403 |
+
"type": "object",
|
| 404 |
+
"properties": {
|
| 405 |
+
"latitude": {
|
| 406 |
+
"type": "number",
|
| 407 |
+
"format": "float"
|
| 408 |
+
},
|
| 409 |
+
"longitude": {
|
| 410 |
+
"type": "number",
|
| 411 |
+
"format": "float"
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
}
|
| 416 |
+
}
|
| 417 |
+
}
|
| 418 |
+
},
|
| 419 |
+
"ValidationRulesResponse": {
|
| 420 |
+
"type": "object",
|
| 421 |
+
"properties": {
|
| 422 |
+
"success": {
|
| 423 |
+
"type": "boolean",
|
| 424 |
+
"example": True
|
| 425 |
+
},
|
| 426 |
+
"message": {
|
| 427 |
+
"type": "string",
|
| 428 |
+
"example": "Current validation rules"
|
| 429 |
+
},
|
| 430 |
+
"data": {
|
| 431 |
+
"type": "object",
|
| 432 |
+
"properties": {
|
| 433 |
+
"blur": {
|
| 434 |
+
"type": "object",
|
| 435 |
+
"description": "Blur detection rules (25% weight)"
|
| 436 |
+
},
|
| 437 |
+
"brightness": {
|
| 438 |
+
"type": "object",
|
| 439 |
+
"description": "Brightness validation rules (20% weight)"
|
| 440 |
+
},
|
| 441 |
+
"resolution": {
|
| 442 |
+
"type": "object",
|
| 443 |
+
"description": "Resolution check rules (25% weight)"
|
| 444 |
+
},
|
| 445 |
+
"exposure": {
|
| 446 |
+
"type": "object",
|
| 447 |
+
"description": "Exposure analysis rules (15% weight)"
|
| 448 |
+
},
|
| 449 |
+
"metadata": {
|
| 450 |
+
"type": "object",
|
| 451 |
+
"description": "Metadata extraction rules (15% weight)"
|
| 452 |
+
}
|
| 453 |
+
}
|
| 454 |
+
}
|
| 455 |
+
}
|
| 456 |
+
},
|
| 457 |
+
"SummaryResponse": {
|
| 458 |
+
"type": "object",
|
| 459 |
+
"properties": {
|
| 460 |
+
"success": {
|
| 461 |
+
"type": "boolean",
|
| 462 |
+
"example": True
|
| 463 |
+
},
|
| 464 |
+
"message": {
|
| 465 |
+
"type": "string",
|
| 466 |
+
"example": "Processing summary retrieved"
|
| 467 |
+
},
|
| 468 |
+
"data": {
|
| 469 |
+
"type": "object",
|
| 470 |
+
"properties": {
|
| 471 |
+
"total_processed": {
|
| 472 |
+
"type": "integer",
|
| 473 |
+
"description": "Total number of images processed",
|
| 474 |
+
"example": 150
|
| 475 |
+
},
|
| 476 |
+
"passed": {
|
| 477 |
+
"type": "integer",
|
| 478 |
+
"description": "Number of images that passed validation",
|
| 479 |
+
"example": 98
|
| 480 |
+
},
|
| 481 |
+
"failed": {
|
| 482 |
+
"type": "integer",
|
| 483 |
+
"description": "Number of images that failed validation",
|
| 484 |
+
"example": 52
|
| 485 |
+
},
|
| 486 |
+
"average_score": {
|
| 487 |
+
"type": "number",
|
| 488 |
+
"format": "float",
|
| 489 |
+
"description": "Average quality score across all processed images",
|
| 490 |
+
"example": 72.5
|
| 491 |
+
}
|
| 492 |
+
}
|
| 493 |
+
}
|
| 494 |
+
}
|
| 495 |
+
},
|
| 496 |
+
"HealthResponse": {
|
| 497 |
+
"type": "object",
|
| 498 |
+
"properties": {
|
| 499 |
+
"success": {
|
| 500 |
+
"type": "boolean",
|
| 501 |
+
"example": True
|
| 502 |
+
},
|
| 503 |
+
"message": {
|
| 504 |
+
"type": "string",
|
| 505 |
+
"example": "Service is running"
|
| 506 |
+
},
|
| 507 |
+
"data": {
|
| 508 |
+
"type": "object",
|
| 509 |
+
"properties": {
|
| 510 |
+
"status": {
|
| 511 |
+
"type": "string",
|
| 512 |
+
"example": "healthy"
|
| 513 |
+
},
|
| 514 |
+
"service": {
|
| 515 |
+
"type": "string",
|
| 516 |
+
"example": "photoguard"
|
| 517 |
+
},
|
| 518 |
+
"api_version": {
|
| 519 |
+
"type": "string",
|
| 520 |
+
"example": "3.0.0"
|
| 521 |
+
}
|
| 522 |
+
}
|
| 523 |
+
}
|
| 524 |
+
}
|
| 525 |
+
},
|
| 526 |
+
"ErrorResponse": {
|
| 527 |
+
"type": "object",
|
| 528 |
+
"properties": {
|
| 529 |
+
"success": {
|
| 530 |
+
"type": "boolean",
|
| 531 |
+
"example": False
|
| 532 |
+
},
|
| 533 |
+
"message": {
|
| 534 |
+
"type": "string",
|
| 535 |
+
"description": "Error description",
|
| 536 |
+
"example": "File type not allowed"
|
| 537 |
+
}
|
| 538 |
+
}
|
| 539 |
+
}
|
| 540 |
+
}
|
| 541 |
+
}
|
| 542 |
+
}
|
app/routes/upload.py
CHANGED
|
@@ -1,278 +1,151 @@
|
|
| 1 |
-
from flask import Blueprint,
|
| 2 |
import os
|
| 3 |
import uuid
|
| 4 |
-
from werkzeug.
|
| 5 |
from werkzeug.exceptions import RequestEntityTooLarge
|
|
|
|
| 6 |
|
| 7 |
from app.services.quality_control import QualityControlService
|
| 8 |
from app.utils.response_formatter import ResponseFormatter
|
|
|
|
|
|
|
| 9 |
|
| 10 |
upload_bp = Blueprint('upload', __name__)
|
| 11 |
|
| 12 |
-
def allowed_file(filename):
|
| 13 |
-
"""Check if file extension is allowed."""
|
| 14 |
-
return '.' in filename and \
|
| 15 |
-
filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
"""Upload and validate image endpoint."""
|
| 20 |
-
try:
|
| 21 |
-
# Check if file is in request
|
| 22 |
-
if 'image' not in request.files:
|
| 23 |
-
return ResponseFormatter.error("No image file provided", 400)
|
| 24 |
-
|
| 25 |
-
file = request.files['image']
|
| 26 |
-
|
| 27 |
-
# Check if file is selected
|
| 28 |
-
if file.filename == '':
|
| 29 |
-
return ResponseFormatter.error("No file selected", 400)
|
| 30 |
-
|
| 31 |
-
# Check file type
|
| 32 |
-
if not allowed_file(file.filename):
|
| 33 |
-
return ResponseFormatter.error(
|
| 34 |
-
f"File type not allowed. Allowed types: {', '.join(current_app.config['ALLOWED_EXTENSIONS'])}",
|
| 35 |
-
400
|
| 36 |
-
)
|
| 37 |
-
|
| 38 |
-
# Generate unique filename
|
| 39 |
-
filename = secure_filename(file.filename)
|
| 40 |
-
unique_filename = f"{uuid.uuid4()}_{filename}"
|
| 41 |
-
filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename)
|
| 42 |
-
|
| 43 |
-
# Save file
|
| 44 |
-
file.save(filepath)
|
| 45 |
-
|
| 46 |
-
# Initialize quality control service
|
| 47 |
-
qc_service = QualityControlService(current_app.config)
|
| 48 |
-
|
| 49 |
-
# Validate image
|
| 50 |
-
validation_results = qc_service.validate_image(filepath)
|
| 51 |
-
|
| 52 |
-
# Format response
|
| 53 |
-
return ResponseFormatter.success(
|
| 54 |
-
data=validation_results,
|
| 55 |
-
message="Image validation completed"
|
| 56 |
-
)
|
| 57 |
-
|
| 58 |
-
except RequestEntityTooLarge:
|
| 59 |
-
return ResponseFormatter.error("File too large", 413)
|
| 60 |
-
except Exception as e:
|
| 61 |
-
return ResponseFormatter.error(f"Upload failed: {str(e)}", 500)
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
try:
|
| 67 |
-
data = request.get_json()
|
| 68 |
-
if not data or 'url' not in data:
|
| 69 |
-
return ResponseFormatter.error("No URL provided", 400)
|
| 70 |
-
|
| 71 |
-
url = data['url']
|
| 72 |
-
|
| 73 |
-
# Download image from URL (implement this as needed)
|
| 74 |
-
# For now, return not implemented
|
| 75 |
-
return ResponseFormatter.error("URL validation not yet implemented", 501)
|
| 76 |
-
|
| 77 |
-
except Exception as e:
|
| 78 |
-
return ResponseFormatter.error(f"URL validation failed: {str(e)}", 500)
|
| 79 |
|
| 80 |
-
@upload_bp.route('/summary', methods=['GET'])
|
| 81 |
-
def get_validation_summary():
|
| 82 |
-
"""Get validation statistics summary."""
|
| 83 |
-
try:
|
| 84 |
-
qc_service = QualityControlService(current_app.config)
|
| 85 |
-
summary = qc_service.get_validation_summary()
|
| 86 |
-
|
| 87 |
-
return ResponseFormatter.success(
|
| 88 |
-
data=summary,
|
| 89 |
-
message="Validation summary retrieved"
|
| 90 |
-
)
|
| 91 |
-
|
| 92 |
-
except Exception as e:
|
| 93 |
-
return ResponseFormatter.error(f"Failed to get summary: {str(e)}", 500)
|
| 94 |
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
@upload_bp.route('/validate', methods=['POST'])
|
| 101 |
def validate_image_api():
|
| 102 |
-
"""
|
| 103 |
-
Comprehensive image validation API endpoint.
|
| 104 |
-
Returns detailed JSON results with all quality checks.
|
| 105 |
-
"""
|
| 106 |
try:
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
file = request.files['image']
|
| 112 |
-
|
| 113 |
-
# Check if file is selected
|
| 114 |
-
if file.filename == '':
|
| 115 |
-
return ResponseFormatter.error("No file selected", 400)
|
| 116 |
-
|
| 117 |
-
# Check file type
|
| 118 |
-
if not allowed_file(file.filename):
|
| 119 |
-
return ResponseFormatter.error(
|
| 120 |
-
f"File type not allowed. Allowed types: {', '.join(current_app.config['ALLOWED_EXTENSIONS'])}",
|
| 121 |
-
400
|
| 122 |
-
)
|
| 123 |
-
|
| 124 |
-
# Generate unique filename
|
| 125 |
-
filename = secure_filename(file.filename)
|
| 126 |
-
unique_filename = f"{uuid.uuid4()}_{filename}"
|
| 127 |
-
filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename)
|
| 128 |
-
|
| 129 |
-
# Save file
|
| 130 |
-
file.save(filepath)
|
| 131 |
-
|
| 132 |
-
# Initialize quality control service
|
| 133 |
qc_service = QualityControlService(current_app.config)
|
| 134 |
-
|
| 135 |
-
# Validate image with new rules
|
| 136 |
validation_results = qc_service.validate_image_with_new_rules(filepath)
|
| 137 |
-
|
| 138 |
-
# Move image based on validation results
|
| 139 |
qc_service.handle_validated_image(filepath, validation_results)
|
| 140 |
-
|
| 141 |
-
# Format response in the new structure
|
| 142 |
response_data = {
|
| 143 |
"summary": {
|
| 144 |
-
"overall_status": validation_results
|
| 145 |
-
"overall_score": validation_results
|
| 146 |
-
"issues_found": validation_results
|
| 147 |
-
"recommendations": validation_results
|
| 148 |
},
|
| 149 |
-
"checks": validation_results
|
| 150 |
}
|
| 151 |
-
|
| 152 |
return ResponseFormatter.success(
|
| 153 |
data=response_data,
|
| 154 |
message="Image validation completed"
|
| 155 |
)
|
| 156 |
-
|
|
|
|
|
|
|
| 157 |
except RequestEntityTooLarge:
|
| 158 |
return ResponseFormatter.error("File too large", 413)
|
| 159 |
-
except Exception as
|
| 160 |
-
return ResponseFormatter.error(f"Validation failed: {
|
|
|
|
| 161 |
|
| 162 |
@upload_bp.route('/validation-rules', methods=['GET'])
|
| 163 |
def get_validation_rules():
|
| 164 |
-
"""
|
| 165 |
from config import Config
|
| 166 |
config = Config()
|
| 167 |
-
|
| 168 |
return ResponseFormatter.success(
|
| 169 |
data=config.VALIDATION_RULES,
|
| 170 |
message="Current validation rules"
|
| 171 |
)
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
test_results = {
|
| 177 |
-
"api_version": "2.0",
|
| 178 |
-
"timestamp": "2025-09-25T11:00:00Z",
|
| 179 |
-
"validation_rules_applied": {
|
| 180 |
-
"blur": "variance_of_laplacian >= 100 (mobile-friendly)",
|
| 181 |
-
"brightness": "mean_pixel_intensity 50-220 (expanded range)",
|
| 182 |
-
"resolution": "min 800x600, >= 0.5MP (mobile-friendly)",
|
| 183 |
-
"exposure": "dynamic_range >= 100, clipping <= 2%",
|
| 184 |
-
"metadata": "6 required fields, >= 15% completeness",
|
| 185 |
-
"overall": "weighted scoring system, pass at >= 65% overall score"
|
| 186 |
-
},
|
| 187 |
-
"endpoints": {
|
| 188 |
-
"POST /api/validate": "Main validation endpoint",
|
| 189 |
-
"POST /api/upload": "Legacy upload endpoint",
|
| 190 |
-
"GET /api/validation-rules": "Get current validation rules",
|
| 191 |
-
"GET /api/test-api": "This test endpoint",
|
| 192 |
-
"GET /api/health": "Health check",
|
| 193 |
-
"GET /api/summary": "Processing statistics"
|
| 194 |
-
},
|
| 195 |
-
"example_response_structure": {
|
| 196 |
-
"success": True,
|
| 197 |
-
"message": "Image validation completed",
|
| 198 |
-
"data": {
|
| 199 |
-
"summary": {
|
| 200 |
-
"overall_status": "pass|fail",
|
| 201 |
-
"overall_score": 80.0,
|
| 202 |
-
"issues_found": 1,
|
| 203 |
-
"recommendations": [
|
| 204 |
-
"Use higher resolution camera setting",
|
| 205 |
-
"Ensure camera metadata is enabled"
|
| 206 |
-
]
|
| 207 |
-
},
|
| 208 |
-
"checks": {
|
| 209 |
-
"blur": {
|
| 210 |
-
"status": "pass|fail",
|
| 211 |
-
"score": 253.96,
|
| 212 |
-
"threshold": 150,
|
| 213 |
-
"reason": "Image sharpness is acceptable"
|
| 214 |
-
},
|
| 215 |
-
"brightness": {
|
| 216 |
-
"status": "pass|fail",
|
| 217 |
-
"mean_brightness": 128.94,
|
| 218 |
-
"range": [90, 180],
|
| 219 |
-
"reason": "Brightness is within the acceptable range"
|
| 220 |
-
},
|
| 221 |
-
"exposure": {
|
| 222 |
-
"status": "pass|fail",
|
| 223 |
-
"dynamic_range": 254,
|
| 224 |
-
"threshold": 150,
|
| 225 |
-
"reason": "Exposure and dynamic range are excellent"
|
| 226 |
-
},
|
| 227 |
-
"resolution": {
|
| 228 |
-
"status": "pass|fail",
|
| 229 |
-
"width": 1184,
|
| 230 |
-
"height": 864,
|
| 231 |
-
"megapixels": 1.02,
|
| 232 |
-
"min_required": "1024x1024, ≥1 MP",
|
| 233 |
-
"reason": "Resolution below minimum required size"
|
| 234 |
-
},
|
| 235 |
-
"metadata": {
|
| 236 |
-
"status": "pass|fail",
|
| 237 |
-
"completeness": 33.3,
|
| 238 |
-
"required_min": 30,
|
| 239 |
-
"missing_fields": ["timestamp", "camera_make_model", "orientation", "iso", "shutter_speed", "aperture"],
|
| 240 |
-
"reason": "Sufficient metadata extracted"
|
| 241 |
-
}
|
| 242 |
-
}
|
| 243 |
-
}
|
| 244 |
-
}
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
return ResponseFormatter.success(
|
| 248 |
-
data=test_results,
|
| 249 |
-
message="API test information and example response structure"
|
| 250 |
-
)
|
| 251 |
|
| 252 |
@upload_bp.route('/summary', methods=['GET'])
|
| 253 |
def get_processing_summary():
|
| 254 |
-
"""
|
| 255 |
try:
|
| 256 |
qc_service = QualityControlService(current_app.config)
|
| 257 |
summary = qc_service.get_validation_summary()
|
| 258 |
-
|
| 259 |
return ResponseFormatter.success(
|
| 260 |
data=summary,
|
| 261 |
message="Processing summary retrieved"
|
| 262 |
)
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
|
| 267 |
@upload_bp.route('/health', methods=['GET'])
|
| 268 |
def health_check():
|
| 269 |
-
"""
|
| 270 |
return ResponseFormatter.success(
|
| 271 |
data={
|
| 272 |
-
"status": "healthy",
|
| 273 |
-
"service": "
|
| 274 |
-
"api_version": "
|
| 275 |
"validation_rules": "updated"
|
| 276 |
},
|
| 277 |
-
message="Service is running
|
| 278 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, current_app, render_template, request, jsonify
|
| 2 |
import os
|
| 3 |
import uuid
|
| 4 |
+
from werkzeug.datastructures import FileStorage
|
| 5 |
from werkzeug.exceptions import RequestEntityTooLarge
|
| 6 |
+
from werkzeug.utils import secure_filename
|
| 7 |
|
| 8 |
from app.services.quality_control import QualityControlService
|
| 9 |
from app.utils.response_formatter import ResponseFormatter
|
| 10 |
+
from app.api_spec import OPENAPI_SPEC
|
| 11 |
+
|
| 12 |
|
| 13 |
upload_bp = Blueprint('upload', __name__)
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
class UploadError(Exception):
|
| 17 |
+
"""Exception raised when an upload request is invalid."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
def __init__(self, message: str, status_code: int = 400) -> None:
|
| 20 |
+
super().__init__(message)
|
| 21 |
+
self.status_code = status_code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
def allowed_file(filename: str) -> bool:
|
| 25 |
+
"""Return True when the provided filename has an allowed extension."""
|
| 26 |
+
if '.' not in filename:
|
| 27 |
+
return False
|
| 28 |
+
extension = filename.rsplit('.', 1)[1].lower()
|
| 29 |
+
return extension in current_app.config['ALLOWED_EXTENSIONS']
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _extract_upload() -> FileStorage:
|
| 33 |
+
"""Pull the uploaded file from the request or raise UploadError."""
|
| 34 |
+
if 'image' not in request.files:
|
| 35 |
+
raise UploadError("No image file provided", status_code=400)
|
| 36 |
+
|
| 37 |
+
file_storage = request.files['image']
|
| 38 |
+
if not isinstance(file_storage, FileStorage) or file_storage.filename == '':
|
| 39 |
+
raise UploadError("No file selected", status_code=400)
|
| 40 |
+
|
| 41 |
+
if not allowed_file(file_storage.filename):
|
| 42 |
+
allowed = ', '.join(sorted(current_app.config['ALLOWED_EXTENSIONS']))
|
| 43 |
+
raise UploadError(f"File type not allowed. Allowed types: {allowed}")
|
| 44 |
+
|
| 45 |
+
return file_storage
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _store_upload(file_storage: FileStorage) -> str:
|
| 49 |
+
"""Persist the uploaded file to the configured upload directory."""
|
| 50 |
+
upload_dir = current_app.config['UPLOAD_FOLDER']
|
| 51 |
+
os.makedirs(upload_dir, exist_ok=True)
|
| 52 |
+
|
| 53 |
+
filename = secure_filename(file_storage.filename)
|
| 54 |
+
unique_filename = f"{uuid.uuid4()}_{filename}"
|
| 55 |
+
filepath = os.path.join(upload_dir, unique_filename)
|
| 56 |
+
file_storage.save(filepath)
|
| 57 |
+
return filepath
|
| 58 |
+
|
| 59 |
|
| 60 |
@upload_bp.route('/validate', methods=['POST'])
|
| 61 |
def validate_image_api():
|
| 62 |
+
"""Validate an uploaded image and return the consolidated scoring payload."""
|
|
|
|
|
|
|
|
|
|
| 63 |
try:
|
| 64 |
+
file_storage = _extract_upload()
|
| 65 |
+
filepath = _store_upload(file_storage)
|
| 66 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
qc_service = QualityControlService(current_app.config)
|
|
|
|
|
|
|
| 68 |
validation_results = qc_service.validate_image_with_new_rules(filepath)
|
|
|
|
|
|
|
| 69 |
qc_service.handle_validated_image(filepath, validation_results)
|
| 70 |
+
|
|
|
|
| 71 |
response_data = {
|
| 72 |
"summary": {
|
| 73 |
+
"overall_status": validation_results.get('overall_status'),
|
| 74 |
+
"overall_score": validation_results.get('overall_score'),
|
| 75 |
+
"issues_found": validation_results.get('issues_found'),
|
| 76 |
+
"recommendations": validation_results.get('recommendations', []),
|
| 77 |
},
|
| 78 |
+
"checks": validation_results.get('checks', {}),
|
| 79 |
}
|
| 80 |
+
|
| 81 |
return ResponseFormatter.success(
|
| 82 |
data=response_data,
|
| 83 |
message="Image validation completed"
|
| 84 |
)
|
| 85 |
+
|
| 86 |
+
except UploadError as exc:
|
| 87 |
+
return ResponseFormatter.error(str(exc), exc.status_code)
|
| 88 |
except RequestEntityTooLarge:
|
| 89 |
return ResponseFormatter.error("File too large", 413)
|
| 90 |
+
except Exception as exc: # pragma: no cover - defensive safeguard
|
| 91 |
+
return ResponseFormatter.error(f"Validation failed: {exc}", 500)
|
| 92 |
+
|
| 93 |
|
| 94 |
@upload_bp.route('/validation-rules', methods=['GET'])
|
| 95 |
def get_validation_rules():
|
| 96 |
+
"""Expose the active validation rules for clients and documentation."""
|
| 97 |
from config import Config
|
| 98 |
config = Config()
|
|
|
|
| 99 |
return ResponseFormatter.success(
|
| 100 |
data=config.VALIDATION_RULES,
|
| 101 |
message="Current validation rules"
|
| 102 |
)
|
| 103 |
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
@upload_bp.route('/summary', methods=['GET'])
|
| 109 |
def get_processing_summary():
|
| 110 |
+
"""Return aggregate validation statistics for observability dashboards."""
|
| 111 |
try:
|
| 112 |
qc_service = QualityControlService(current_app.config)
|
| 113 |
summary = qc_service.get_validation_summary()
|
|
|
|
| 114 |
return ResponseFormatter.success(
|
| 115 |
data=summary,
|
| 116 |
message="Processing summary retrieved"
|
| 117 |
)
|
| 118 |
+
except Exception as exc: # pragma: no cover - defensive safeguard
|
| 119 |
+
return ResponseFormatter.error(f"Failed to get summary: {exc}", 500)
|
| 120 |
+
|
| 121 |
|
| 122 |
@upload_bp.route('/health', methods=['GET'])
|
| 123 |
def health_check():
|
| 124 |
+
"""Simple health check endpoint for load balancers and monitors."""
|
| 125 |
return ResponseFormatter.success(
|
| 126 |
data={
|
| 127 |
+
"status": "healthy",
|
| 128 |
+
"service": "photoguard",
|
| 129 |
+
"api_version": "3.0.0",
|
| 130 |
"validation_rules": "updated"
|
| 131 |
},
|
| 132 |
+
message="Service is running"
|
| 133 |
)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@upload_bp.route('/openapi.json', methods=['GET'])
|
| 137 |
+
def get_openapi_spec():
|
| 138 |
+
"""Return the OpenAPI 3.1 specification in JSON format."""
|
| 139 |
+
return jsonify(OPENAPI_SPEC)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
@upload_bp.route('/docs', methods=['GET'])
|
| 143 |
+
def swagger_ui():
|
| 144 |
+
"""Serve the Swagger UI documentation page."""
|
| 145 |
+
return render_template('swagger.html')
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
@upload_bp.route('/redoc', methods=['GET'])
|
| 149 |
+
def redoc_ui():
|
| 150 |
+
"""Serve the ReDoc documentation page."""
|
| 151 |
+
return render_template('redoc.html')
|
app/services/quality_control.py
CHANGED
|
@@ -22,41 +22,20 @@ class QualityControlService:
|
|
| 22 |
# Config class instance
|
| 23 |
self.processed_folder = config.PROCESSED_FOLDER
|
| 24 |
self.rejected_folder = config.REJECTED_FOLDER
|
| 25 |
-
self.yolo_model_path = config.YOLO_MODEL_PATH
|
| 26 |
self.blur_threshold = config.BLUR_THRESHOLD
|
| 27 |
self.min_brightness = config.MIN_BRIGHTNESS
|
| 28 |
self.max_brightness = config.MAX_BRIGHTNESS
|
| 29 |
self.min_resolution_width = config.MIN_RESOLUTION_WIDTH
|
| 30 |
self.min_resolution_height = config.MIN_RESOLUTION_HEIGHT
|
| 31 |
-
self.city_boundaries = config.CITY_BOUNDARIES
|
| 32 |
else:
|
| 33 |
# Flask config object (dictionary-like)
|
| 34 |
self.processed_folder = config.get('PROCESSED_FOLDER', 'storage/processed')
|
| 35 |
self.rejected_folder = config.get('REJECTED_FOLDER', 'storage/rejected')
|
| 36 |
-
self.yolo_model_path = config.get('YOLO_MODEL_PATH', 'models/yolov8n.pt')
|
| 37 |
self.blur_threshold = config.get('BLUR_THRESHOLD', 100.0)
|
| 38 |
self.min_brightness = config.get('MIN_BRIGHTNESS', 30)
|
| 39 |
self.max_brightness = config.get('MAX_BRIGHTNESS', 220)
|
| 40 |
self.min_resolution_width = config.get('MIN_RESOLUTION_WIDTH', 800)
|
| 41 |
self.min_resolution_height = config.get('MIN_RESOLUTION_HEIGHT', 600)
|
| 42 |
-
self.city_boundaries = config.get('CITY_BOUNDARIES', {
|
| 43 |
-
'min_lat': 40.4774,
|
| 44 |
-
'max_lat': 40.9176,
|
| 45 |
-
'min_lon': -74.2591,
|
| 46 |
-
'max_lon': -73.7004
|
| 47 |
-
})
|
| 48 |
-
|
| 49 |
-
self.object_detector = None
|
| 50 |
-
self._initialize_object_detector()
|
| 51 |
-
|
| 52 |
-
def _initialize_object_detector(self):
|
| 53 |
-
"""Initialize object detector if model exists."""
|
| 54 |
-
try:
|
| 55 |
-
if os.path.exists(self.yolo_model_path):
|
| 56 |
-
from app.utils.object_detection import ObjectDetector
|
| 57 |
-
self.object_detector = ObjectDetector(self.yolo_model_path)
|
| 58 |
-
except Exception as e:
|
| 59 |
-
print(f"Warning: Object detector initialization failed: {e}")
|
| 60 |
|
| 61 |
def validate_image(self, image_path: str) -> Dict:
|
| 62 |
"""
|
|
@@ -82,8 +61,7 @@ class QualityControlService:
|
|
| 82 |
"blur_detection": None,
|
| 83 |
"brightness_validation": None,
|
| 84 |
"resolution_check": None,
|
| 85 |
-
"metadata_extraction": None
|
| 86 |
-
"object_detection": None
|
| 87 |
},
|
| 88 |
"metrics": {},
|
| 89 |
"recommendations": []
|
|
@@ -187,38 +165,9 @@ class QualityControlService:
|
|
| 187 |
metadata = MetadataExtractor.extract_metadata(image_path)
|
| 188 |
results["validations"]["metadata_extraction"] = metadata
|
| 189 |
|
| 190 |
-
# Check GPS location if available
|
| 191 |
-
if metadata.get("gps_data"):
|
| 192 |
-
location_validation = MetadataExtractor.validate_location(
|
| 193 |
-
metadata["gps_data"], self.city_boundaries
|
| 194 |
-
)
|
| 195 |
-
if not location_validation["within_boundaries"]:
|
| 196 |
-
results["warnings"].append({
|
| 197 |
-
"type": "location",
|
| 198 |
-
"message": location_validation["reason"]
|
| 199 |
-
})
|
| 200 |
-
|
| 201 |
except Exception as e:
|
| 202 |
results["validations"]["metadata_extraction"] = {"error": str(e)}
|
| 203 |
-
results["warnings"].append(f"Metadata extraction failed: {str(e)}")
|
| 204 |
-
if self.object_detector:
|
| 205 |
-
try:
|
| 206 |
-
detection_results = self.object_detector.detect_objects(image_path)
|
| 207 |
-
results["validations"]["object_detection"] = detection_results
|
| 208 |
-
|
| 209 |
-
if not detection_results["has_civic_content"]:
|
| 210 |
-
results["warnings"].append({
|
| 211 |
-
"type": "civic_content",
|
| 212 |
-
"message": "No civic-related objects detected in image"
|
| 213 |
-
})
|
| 214 |
-
|
| 215 |
-
except Exception as e:
|
| 216 |
-
results["validations"]["object_detection"] = {"error": str(e)}
|
| 217 |
-
results["warnings"].append(f"Object detection failed: {str(e)}")
|
| 218 |
-
else:
|
| 219 |
-
results["validations"]["object_detection"] = {
|
| 220 |
-
"message": "Object detection not available - model not loaded"
|
| 221 |
-
}
|
| 222 |
|
| 223 |
# Calculate overall metrics
|
| 224 |
results["metrics"] = self._calculate_metrics(results)
|
|
|
|
| 22 |
# Config class instance
|
| 23 |
self.processed_folder = config.PROCESSED_FOLDER
|
| 24 |
self.rejected_folder = config.REJECTED_FOLDER
|
|
|
|
| 25 |
self.blur_threshold = config.BLUR_THRESHOLD
|
| 26 |
self.min_brightness = config.MIN_BRIGHTNESS
|
| 27 |
self.max_brightness = config.MAX_BRIGHTNESS
|
| 28 |
self.min_resolution_width = config.MIN_RESOLUTION_WIDTH
|
| 29 |
self.min_resolution_height = config.MIN_RESOLUTION_HEIGHT
|
|
|
|
| 30 |
else:
|
| 31 |
# Flask config object (dictionary-like)
|
| 32 |
self.processed_folder = config.get('PROCESSED_FOLDER', 'storage/processed')
|
| 33 |
self.rejected_folder = config.get('REJECTED_FOLDER', 'storage/rejected')
|
|
|
|
| 34 |
self.blur_threshold = config.get('BLUR_THRESHOLD', 100.0)
|
| 35 |
self.min_brightness = config.get('MIN_BRIGHTNESS', 30)
|
| 36 |
self.max_brightness = config.get('MAX_BRIGHTNESS', 220)
|
| 37 |
self.min_resolution_width = config.get('MIN_RESOLUTION_WIDTH', 800)
|
| 38 |
self.min_resolution_height = config.get('MIN_RESOLUTION_HEIGHT', 600)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
def validate_image(self, image_path: str) -> Dict:
|
| 41 |
"""
|
|
|
|
| 61 |
"blur_detection": None,
|
| 62 |
"brightness_validation": None,
|
| 63 |
"resolution_check": None,
|
| 64 |
+
"metadata_extraction": None
|
|
|
|
| 65 |
},
|
| 66 |
"metrics": {},
|
| 67 |
"recommendations": []
|
|
|
|
| 165 |
metadata = MetadataExtractor.extract_metadata(image_path)
|
| 166 |
results["validations"]["metadata_extraction"] = metadata
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
except Exception as e:
|
| 169 |
results["validations"]["metadata_extraction"] = {"error": str(e)}
|
| 170 |
+
results["warnings"].append(f"Metadata extraction failed: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
# Calculate overall metrics
|
| 173 |
results["metrics"] = self._calculate_metrics(results)
|
app/utils/blur_detection.py
CHANGED
|
@@ -1,6 +1,35 @@
|
|
| 1 |
import cv2
|
| 2 |
import numpy as np
|
| 3 |
-
from typing import Tuple
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
class BlurDetector:
|
| 6 |
"""Detects image blur using Laplacian variance method."""
|
|
|
|
| 1 |
import cv2
|
| 2 |
import numpy as np
|
| 3 |
+
from typing import Optional, Tuple, Union
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def blur_score(image: Optional[Union[str, bytes, np.ndarray]]) -> float:
|
| 7 |
+
"""Return Laplacian variance for backward-compatible API calls.
|
| 8 |
+
|
| 9 |
+
Accepts either a path to an image on disk or an in-memory numpy array.
|
| 10 |
+
When the input is ``None`` or invalid, ``0.0`` is returned so legacy
|
| 11 |
+
callers can treat the output as a failed blur computation.
|
| 12 |
+
"""
|
| 13 |
+
if image is None:
|
| 14 |
+
return 0.0
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
if isinstance(image, (str, bytes)):
|
| 18 |
+
frame = cv2.imread(image)
|
| 19 |
+
else:
|
| 20 |
+
frame = np.asarray(image)
|
| 21 |
+
|
| 22 |
+
if frame is None or frame.size == 0:
|
| 23 |
+
return 0.0
|
| 24 |
+
|
| 25 |
+
if frame.ndim == 3:
|
| 26 |
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
| 27 |
+
else:
|
| 28 |
+
gray = frame
|
| 29 |
+
|
| 30 |
+
return float(cv2.Laplacian(gray, cv2.CV_64F).var())
|
| 31 |
+
except Exception:
|
| 32 |
+
return 0.0
|
| 33 |
|
| 34 |
class BlurDetector:
|
| 35 |
"""Detects image blur using Laplacian variance method."""
|
app/utils/image_validation.py
DELETED
|
@@ -1,77 +0,0 @@
|
|
| 1 |
-
from .blur_detection import BlurDetector
|
| 2 |
-
from .brightness_validation import BrightnessValidator
|
| 3 |
-
from .resolution_check import ResolutionChecker
|
| 4 |
-
from .metadata_extraction import MetadataExtractor
|
| 5 |
-
from .object_detection import ObjectDetector
|
| 6 |
-
|
| 7 |
-
class ImageValidator:
|
| 8 |
-
"""Combined image validation class for legacy compatibility."""
|
| 9 |
-
|
| 10 |
-
def __init__(self, blur_threshold=100, brightness_min=40, brightness_max=220, min_width=800, min_height=600):
|
| 11 |
-
self.blur_threshold = blur_threshold
|
| 12 |
-
self.brightness_min = brightness_min
|
| 13 |
-
self.brightness_max = brightness_max
|
| 14 |
-
self.min_width = min_width
|
| 15 |
-
self.min_height = min_height
|
| 16 |
-
|
| 17 |
-
def validate_image(self, image_path: str) -> dict:
|
| 18 |
-
"""
|
| 19 |
-
Validate image and return comprehensive results.
|
| 20 |
-
|
| 21 |
-
Args:
|
| 22 |
-
image_path (str): Path to the image file
|
| 23 |
-
|
| 24 |
-
Returns:
|
| 25 |
-
dict: Validation results
|
| 26 |
-
"""
|
| 27 |
-
results = {
|
| 28 |
-
"blur": None,
|
| 29 |
-
"brightness": None,
|
| 30 |
-
"resolution": None,
|
| 31 |
-
"metadata": None,
|
| 32 |
-
"objects": None,
|
| 33 |
-
"overall_status": "UNKNOWN"
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
try:
|
| 37 |
-
# Blur detection
|
| 38 |
-
blur_score, is_blurry = BlurDetector.calculate_blur_score(image_path, self.blur_threshold)
|
| 39 |
-
results["blur"] = BlurDetector.get_blur_details(blur_score, self.blur_threshold)
|
| 40 |
-
|
| 41 |
-
# Brightness validation
|
| 42 |
-
results["brightness"] = BrightnessValidator.analyze_brightness(
|
| 43 |
-
image_path, self.brightness_min, self.brightness_max
|
| 44 |
-
)
|
| 45 |
-
|
| 46 |
-
# Resolution check
|
| 47 |
-
results["resolution"] = ResolutionChecker.analyze_resolution(
|
| 48 |
-
image_path, self.min_width, self.min_height
|
| 49 |
-
)
|
| 50 |
-
|
| 51 |
-
# Metadata extraction
|
| 52 |
-
results["metadata"] = MetadataExtractor.extract_metadata(image_path)
|
| 53 |
-
|
| 54 |
-
# Object detection (if available)
|
| 55 |
-
try:
|
| 56 |
-
detector = ObjectDetector()
|
| 57 |
-
results["objects"] = detector.detect_objects(image_path)
|
| 58 |
-
except:
|
| 59 |
-
results["objects"] = {"error": "Object detection not available"}
|
| 60 |
-
|
| 61 |
-
# Determine overall status
|
| 62 |
-
issues = []
|
| 63 |
-
if results["blur"]["is_blurry"]:
|
| 64 |
-
issues.append("blurry")
|
| 65 |
-
if results["brightness"]["has_brightness_issues"]:
|
| 66 |
-
issues.append("brightness")
|
| 67 |
-
if not results["resolution"]["meets_min_resolution"]:
|
| 68 |
-
issues.append("resolution")
|
| 69 |
-
|
| 70 |
-
results["overall_status"] = "PASS" if not issues else "FAIL"
|
| 71 |
-
results["issues"] = issues
|
| 72 |
-
|
| 73 |
-
except Exception as e:
|
| 74 |
-
results["error"] = str(e)
|
| 75 |
-
results["overall_status"] = "ERROR"
|
| 76 |
-
|
| 77 |
-
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/utils/metadata_extraction.py
CHANGED
|
@@ -255,33 +255,3 @@ class MetadataExtractor:
|
|
| 255 |
"quality_level": quality_level,
|
| 256 |
"meets_requirements": completeness_percentage >= 70
|
| 257 |
}
|
| 258 |
-
|
| 259 |
-
@staticmethod
|
| 260 |
-
def validate_location(gps_data: Dict, boundaries: Dict) -> Dict:
|
| 261 |
-
"""Validate if GPS coordinates are within city boundaries."""
|
| 262 |
-
if not gps_data:
|
| 263 |
-
return {
|
| 264 |
-
"within_boundaries": False,
|
| 265 |
-
"reason": "No GPS data available"
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
lat = gps_data.get("latitude")
|
| 269 |
-
lon = gps_data.get("longitude")
|
| 270 |
-
|
| 271 |
-
if lat is None or lon is None:
|
| 272 |
-
return {
|
| 273 |
-
"within_boundaries": False,
|
| 274 |
-
"reason": "Invalid GPS coordinates"
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
within_bounds = (
|
| 278 |
-
boundaries["min_lat"] <= lat <= boundaries["max_lat"] and
|
| 279 |
-
boundaries["min_lon"] <= lon <= boundaries["max_lon"]
|
| 280 |
-
)
|
| 281 |
-
|
| 282 |
-
return {
|
| 283 |
-
"within_boundaries": within_bounds,
|
| 284 |
-
"latitude": lat,
|
| 285 |
-
"longitude": lon,
|
| 286 |
-
"reason": "Valid location" if within_bounds else "Outside city boundaries"
|
| 287 |
-
}
|
|
|
|
| 255 |
"quality_level": quality_level,
|
| 256 |
"meets_requirements": completeness_percentage >= 70
|
| 257 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/utils/object_detection.py
DELETED
|
@@ -1,140 +0,0 @@
|
|
| 1 |
-
from ultralytics import YOLO
|
| 2 |
-
import cv2
|
| 3 |
-
import numpy as np
|
| 4 |
-
from typing import Dict, List, Tuple
|
| 5 |
-
import os
|
| 6 |
-
|
| 7 |
-
class ObjectDetector:
|
| 8 |
-
"""Handles object detection using YOLO models."""
|
| 9 |
-
|
| 10 |
-
def __init__(self, model_path: str = "models/yolov8n.pt"):
|
| 11 |
-
"""Initialize YOLO model."""
|
| 12 |
-
self.model_path = model_path
|
| 13 |
-
self.model = None
|
| 14 |
-
self._load_model()
|
| 15 |
-
|
| 16 |
-
def _load_model(self):
|
| 17 |
-
"""Load YOLO model."""
|
| 18 |
-
try:
|
| 19 |
-
if os.path.exists(self.model_path):
|
| 20 |
-
self.model = YOLO(self.model_path)
|
| 21 |
-
else:
|
| 22 |
-
# Download model if not exists
|
| 23 |
-
self.model = YOLO("yolov8n.pt")
|
| 24 |
-
# Save to models directory
|
| 25 |
-
os.makedirs("models", exist_ok=True)
|
| 26 |
-
self.model.export(format="onnx") # Optional: export to different format
|
| 27 |
-
except Exception as e:
|
| 28 |
-
raise Exception(f"Failed to load YOLO model: {str(e)}")
|
| 29 |
-
|
| 30 |
-
def detect_objects(self, image_path: str, confidence_threshold: float = 0.5) -> Dict:
|
| 31 |
-
"""
|
| 32 |
-
Detect objects in image using YOLO.
|
| 33 |
-
|
| 34 |
-
Args:
|
| 35 |
-
image_path: Path to the image file
|
| 36 |
-
confidence_threshold: Minimum confidence for detections
|
| 37 |
-
|
| 38 |
-
Returns:
|
| 39 |
-
Dictionary with detection results
|
| 40 |
-
"""
|
| 41 |
-
try:
|
| 42 |
-
if self.model is None:
|
| 43 |
-
raise Exception("YOLO model not loaded")
|
| 44 |
-
|
| 45 |
-
# Run inference
|
| 46 |
-
results = self.model(image_path, conf=confidence_threshold)
|
| 47 |
-
|
| 48 |
-
# Process results
|
| 49 |
-
detections = []
|
| 50 |
-
civic_objects = []
|
| 51 |
-
|
| 52 |
-
for result in results:
|
| 53 |
-
boxes = result.boxes
|
| 54 |
-
if boxes is not None:
|
| 55 |
-
for box in boxes:
|
| 56 |
-
# Get detection details
|
| 57 |
-
confidence = float(box.conf[0])
|
| 58 |
-
class_id = int(box.cls[0])
|
| 59 |
-
class_name = self.model.names[class_id]
|
| 60 |
-
bbox = box.xyxy[0].tolist() # [x1, y1, x2, y2]
|
| 61 |
-
|
| 62 |
-
detection = {
|
| 63 |
-
"class_name": class_name,
|
| 64 |
-
"confidence": round(confidence, 3),
|
| 65 |
-
"bbox": [round(coord, 2) for coord in bbox],
|
| 66 |
-
"area": (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
|
| 67 |
-
}
|
| 68 |
-
detections.append(detection)
|
| 69 |
-
|
| 70 |
-
# Check for civic-related objects
|
| 71 |
-
if self._is_civic_object(class_name):
|
| 72 |
-
civic_objects.append(detection)
|
| 73 |
-
|
| 74 |
-
return {
|
| 75 |
-
"total_detections": len(detections),
|
| 76 |
-
"all_detections": detections,
|
| 77 |
-
"civic_objects": civic_objects,
|
| 78 |
-
"civic_object_count": len(civic_objects),
|
| 79 |
-
"has_civic_content": len(civic_objects) > 0,
|
| 80 |
-
"summary": self._generate_detection_summary(detections)
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
except Exception as e:
|
| 84 |
-
return {
|
| 85 |
-
"error": f"Object detection failed: {str(e)}",
|
| 86 |
-
"total_detections": 0,
|
| 87 |
-
"all_detections": [],
|
| 88 |
-
"civic_objects": [],
|
| 89 |
-
"civic_object_count": 0,
|
| 90 |
-
"has_civic_content": False
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
def _is_civic_object(self, class_name: str) -> bool:
|
| 94 |
-
"""Check if detected object is civic-related."""
|
| 95 |
-
civic_classes = [
|
| 96 |
-
"car", "truck", "bus", "motorcycle", "bicycle",
|
| 97 |
-
"traffic light", "stop sign", "bench", "fire hydrant",
|
| 98 |
-
"street sign", "pothole", "trash can", "dumpster"
|
| 99 |
-
]
|
| 100 |
-
return class_name.lower() in [c.lower() for c in civic_classes]
|
| 101 |
-
|
| 102 |
-
def _generate_detection_summary(self, detections: List[Dict]) -> Dict:
|
| 103 |
-
"""Generate summary of detections."""
|
| 104 |
-
if not detections:
|
| 105 |
-
return {"message": "No objects detected"}
|
| 106 |
-
|
| 107 |
-
# Count objects by class
|
| 108 |
-
class_counts = {}
|
| 109 |
-
for detection in detections:
|
| 110 |
-
class_name = detection["class_name"]
|
| 111 |
-
class_counts[class_name] = class_counts.get(class_name, 0) + 1
|
| 112 |
-
|
| 113 |
-
# Find most confident detection
|
| 114 |
-
most_confident = max(detections, key=lambda x: x["confidence"])
|
| 115 |
-
|
| 116 |
-
return {
|
| 117 |
-
"unique_classes": len(class_counts),
|
| 118 |
-
"class_counts": class_counts,
|
| 119 |
-
"most_confident_detection": {
|
| 120 |
-
"class": most_confident["class_name"],
|
| 121 |
-
"confidence": most_confident["confidence"]
|
| 122 |
-
},
|
| 123 |
-
"avg_confidence": round(
|
| 124 |
-
sum(d["confidence"] for d in detections) / len(detections), 3
|
| 125 |
-
)
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
def detect_specific_civic_issues(self, image_path: str) -> Dict:
|
| 129 |
-
"""
|
| 130 |
-
Detect specific civic issues (future enhancement).
|
| 131 |
-
This would use a fine-tuned model for pothole, overflowing bins, etc.
|
| 132 |
-
"""
|
| 133 |
-
# Placeholder for future implementation
|
| 134 |
-
return {
|
| 135 |
-
"potholes": [],
|
| 136 |
-
"overflowing_bins": [],
|
| 137 |
-
"broken_streetlights": [],
|
| 138 |
-
"graffiti": [],
|
| 139 |
-
"message": "Specific civic issue detection not yet implemented"
|
| 140 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config.py
CHANGED
|
@@ -144,14 +144,6 @@ class Config:
|
|
| 144 |
}
|
| 145 |
}
|
| 146 |
|
| 147 |
-
# ===================================================================
|
| 148 |
-
# MACHINE LEARNING MODEL CONFIGURATION
|
| 149 |
-
# ===================================================================
|
| 150 |
-
|
| 151 |
-
# Path to YOLOv8 object detection model
|
| 152 |
-
# Used for identifying civic-related objects (optional feature)
|
| 153 |
-
YOLO_MODEL_PATH = os.environ.get('YOLO_MODEL_PATH', 'models/yolov8n.pt')
|
| 154 |
-
|
| 155 |
# ===================================================================
|
| 156 |
# FILE TYPE CONFIGURATION
|
| 157 |
# ===================================================================
|
|
@@ -159,20 +151,6 @@ class Config:
|
|
| 159 |
# Allowed image file extensions for upload
|
| 160 |
# Supports common mobile photo formats including HEIC (iOS)
|
| 161 |
ALLOWED_EXTENSIONS = set(os.environ.get('ALLOWED_EXTENSIONS', 'jpg,jpeg,png,bmp,tiff').split(','))
|
| 162 |
-
|
| 163 |
-
# ===================================================================
|
| 164 |
-
# GEOGRAPHIC VALIDATION (Optional Feature)
|
| 165 |
-
# ===================================================================
|
| 166 |
-
|
| 167 |
-
# Geographic boundaries for location validation
|
| 168 |
-
# Example coordinates for New York City area
|
| 169 |
-
# Customize these for your specific civic area
|
| 170 |
-
CITY_BOUNDARIES = {
|
| 171 |
-
'min_lat': 40.4774, # Southern boundary (latitude)
|
| 172 |
-
'max_lat': 40.9176, # Northern boundary (latitude)
|
| 173 |
-
'min_lon': -74.2591, # Western boundary (longitude)
|
| 174 |
-
'max_lon': -73.7004 # Eastern boundary (longitude)
|
| 175 |
-
}
|
| 176 |
|
| 177 |
|
| 178 |
# ===================================================================
|
|
@@ -189,11 +167,14 @@ class ProductionConfig(Config):
|
|
| 189 |
"""Production environment configuration with debug mode disabled."""
|
| 190 |
DEBUG = False
|
| 191 |
TESTING = False
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
|
| 199 |
# Configuration dictionary for easy access
|
|
@@ -201,5 +182,6 @@ class ProductionConfig(Config):
|
|
| 201 |
config = {
|
| 202 |
'development': DevelopmentConfig,
|
| 203 |
'production': ProductionConfig,
|
|
|
|
| 204 |
'default': DevelopmentConfig # Default to development if not specified
|
| 205 |
}
|
|
|
|
| 144 |
}
|
| 145 |
}
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
# ===================================================================
|
| 148 |
# FILE TYPE CONFIGURATION
|
| 149 |
# ===================================================================
|
|
|
|
| 151 |
# Allowed image file extensions for upload
|
| 152 |
# Supports common mobile photo formats including HEIC (iOS)
|
| 153 |
ALLOWED_EXTENSIONS = set(os.environ.get('ALLOWED_EXTENSIONS', 'jpg,jpeg,png,bmp,tiff').split(','))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
|
| 156 |
# ===================================================================
|
|
|
|
| 167 |
"""Production environment configuration with debug mode disabled."""
|
| 168 |
DEBUG = False
|
| 169 |
TESTING = False
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
class TestingConfig(Config):
|
| 173 |
+
"""Testing configuration with deterministic behaviour."""
|
| 174 |
+
DEBUG = False
|
| 175 |
+
TESTING = True
|
| 176 |
+
# Use in-memory friendly limits to keep tests fast
|
| 177 |
+
MAX_CONTENT_LENGTH = int(os.environ.get('MAX_CONTENT_LENGTH', 8 * 1024 * 1024))
|
| 178 |
|
| 179 |
|
| 180 |
# Configuration dictionary for easy access
|
|
|
|
| 182 |
config = {
|
| 183 |
'development': DevelopmentConfig,
|
| 184 |
'production': ProductionConfig,
|
| 185 |
+
'testing': TestingConfig,
|
| 186 |
'default': DevelopmentConfig # Default to development if not specified
|
| 187 |
}
|
docker-compose.yml
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
version: '3.8'
|
| 2 |
-
|
| 3 |
-
services:
|
| 4 |
-
civic-quality:
|
| 5 |
-
build: .
|
| 6 |
-
container_name: civic-quality-app
|
| 7 |
-
ports:
|
| 8 |
-
- "8000:8000"
|
| 9 |
-
environment:
|
| 10 |
-
- SECRET_KEY=${SECRET_KEY:-change-this-in-production}
|
| 11 |
-
- FLASK_ENV=production
|
| 12 |
-
- BLUR_THRESHOLD=80.0
|
| 13 |
-
- MIN_BRIGHTNESS=25
|
| 14 |
-
- MAX_BRIGHTNESS=235
|
| 15 |
-
volumes:
|
| 16 |
-
- ./storage:/app/storage
|
| 17 |
-
- ./logs:/app/logs
|
| 18 |
-
restart: unless-stopped
|
| 19 |
-
healthcheck:
|
| 20 |
-
test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
|
| 21 |
-
interval: 30s
|
| 22 |
-
timeout: 10s
|
| 23 |
-
retries: 3
|
| 24 |
-
start_period: 60s
|
| 25 |
-
|
| 26 |
-
nginx:
|
| 27 |
-
image: nginx:alpine
|
| 28 |
-
container_name: civic-quality-nginx
|
| 29 |
-
ports:
|
| 30 |
-
- "80:80"
|
| 31 |
-
- "443:443"
|
| 32 |
-
volumes:
|
| 33 |
-
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
| 34 |
-
- ./nginx/ssl:/etc/nginx/ssl:ro
|
| 35 |
-
depends_on:
|
| 36 |
-
- civic-quality
|
| 37 |
-
restart: unless-stopped
|
| 38 |
-
|
| 39 |
-
volumes:
|
| 40 |
-
storage:
|
| 41 |
-
logs:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/API.md
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
# API Documentation
|
| 2 |
-
|
| 3 |
-
## Endpoints
|
| 4 |
-
|
| 5 |
-
### POST /check_quality
|
| 6 |
-
|
| 7 |
-
Upload an image for quality control assessment.
|
| 8 |
-
|
| 9 |
-
**Request:**
|
| 10 |
-
- Content-Type: multipart/form-data
|
| 11 |
-
- Body: image file
|
| 12 |
-
|
| 13 |
-
**Response:**
|
| 14 |
-
```json
|
| 15 |
-
{
|
| 16 |
-
"status": "PASS|FAIL",
|
| 17 |
-
"checks": {
|
| 18 |
-
"blur": {
|
| 19 |
-
"value": 150.5,
|
| 20 |
-
"status": "OK"
|
| 21 |
-
},
|
| 22 |
-
"brightness": {
|
| 23 |
-
"value": 128.0,
|
| 24 |
-
"status": "OK"
|
| 25 |
-
},
|
| 26 |
-
"resolution": {
|
| 27 |
-
"value": "1920x1080",
|
| 28 |
-
"status": "OK"
|
| 29 |
-
}
|
| 30 |
-
},
|
| 31 |
-
"metadata": {
|
| 32 |
-
"format": "JPEG",
|
| 33 |
-
"size": [1920, 1080],
|
| 34 |
-
"mode": "RGB"
|
| 35 |
-
},
|
| 36 |
-
"objects": []
|
| 37 |
-
}
|
| 38 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/API_v2.md
DELETED
|
@@ -1,427 +0,0 @@
|
|
| 1 |
-
# Civic Quality Control API Documentation
|
| 2 |
-
|
| 3 |
-
**Version**: 2.0
|
| 4 |
-
**Base URL**: `http://localhost:5000/api` (development) | `http://your-domain.com/api` (production)
|
| 5 |
-
**Content-Type**: `application/json`
|
| 6 |
-
|
| 7 |
-
## 📋 API Overview
|
| 8 |
-
|
| 9 |
-
The Civic Quality Control API provides comprehensive image validation services optimized for mobile photography. It uses a weighted scoring system with partial credit to achieve realistic acceptance rates for civic documentation.
|
| 10 |
-
|
| 11 |
-
### Key Features
|
| 12 |
-
- **Weighted Validation**: 5-component analysis with intelligent scoring
|
| 13 |
-
- **Mobile-Optimized**: Thresholds designed for smartphone cameras
|
| 14 |
-
- **High Performance**: <2 second processing time per image
|
| 15 |
-
- **Comprehensive Feedback**: Detailed validation results and recommendations
|
| 16 |
-
|
| 17 |
-
---
|
| 18 |
-
|
| 19 |
-
## 🔍 Endpoints
|
| 20 |
-
|
| 21 |
-
### 1. Health Check
|
| 22 |
-
|
| 23 |
-
**Endpoint**: `GET /api/health`
|
| 24 |
-
**Purpose**: System status and configuration verification
|
| 25 |
-
|
| 26 |
-
**Response:**
|
| 27 |
-
```json
|
| 28 |
-
{
|
| 29 |
-
"success": true,
|
| 30 |
-
"data": {
|
| 31 |
-
"service": "civic-quality-control",
|
| 32 |
-
"status": "healthy",
|
| 33 |
-
"api_version": "2.0",
|
| 34 |
-
"validation_rules": "updated"
|
| 35 |
-
},
|
| 36 |
-
"message": "Service is running with updated validation rules",
|
| 37 |
-
"error": null
|
| 38 |
-
}
|
| 39 |
-
```
|
| 40 |
-
|
| 41 |
-
**Example:**
|
| 42 |
-
```bash
|
| 43 |
-
curl http://localhost:5000/api/health
|
| 44 |
-
```
|
| 45 |
-
|
| 46 |
-
---
|
| 47 |
-
|
| 48 |
-
### 2. Image Validation (Primary Endpoint)
|
| 49 |
-
|
| 50 |
-
**Endpoint**: `POST /api/validate`
|
| 51 |
-
**Purpose**: Comprehensive image quality validation with weighted scoring
|
| 52 |
-
|
| 53 |
-
**Request:**
|
| 54 |
-
```bash
|
| 55 |
-
Content-Type: multipart/form-data
|
| 56 |
-
Body: image=@your_image.jpg
|
| 57 |
-
```
|
| 58 |
-
|
| 59 |
-
**Response Structure:**
|
| 60 |
-
```json
|
| 61 |
-
{
|
| 62 |
-
"success": true,
|
| 63 |
-
"data": {
|
| 64 |
-
"summary": {
|
| 65 |
-
"overall_status": "PASS|FAIL",
|
| 66 |
-
"overall_score": 85.2,
|
| 67 |
-
"total_issues": 1,
|
| 68 |
-
"image_id": "20250925_143021_abc123_image.jpg"
|
| 69 |
-
},
|
| 70 |
-
"checks": {
|
| 71 |
-
"blur": {
|
| 72 |
-
"status": "PASS|FAIL",
|
| 73 |
-
"score": 95.0,
|
| 74 |
-
"weight": 25,
|
| 75 |
-
"message": "Image sharpness is excellent",
|
| 76 |
-
"details": {
|
| 77 |
-
"variance": 245.6,
|
| 78 |
-
"threshold": 100,
|
| 79 |
-
"quality_level": "excellent"
|
| 80 |
-
}
|
| 81 |
-
},
|
| 82 |
-
"resolution": {
|
| 83 |
-
"status": "PASS|FAIL",
|
| 84 |
-
"score": 100.0,
|
| 85 |
-
"weight": 25,
|
| 86 |
-
"message": "Resolution exceeds requirements",
|
| 87 |
-
"details": {
|
| 88 |
-
"width": 1920,
|
| 89 |
-
"height": 1080,
|
| 90 |
-
"megapixels": 2.07,
|
| 91 |
-
"min_required": 0.5
|
| 92 |
-
}
|
| 93 |
-
},
|
| 94 |
-
"brightness": {
|
| 95 |
-
"status": "PASS|FAIL",
|
| 96 |
-
"score": 80.0,
|
| 97 |
-
"weight": 20,
|
| 98 |
-
"message": "Brightness is within acceptable range",
|
| 99 |
-
"details": {
|
| 100 |
-
"mean_intensity": 142.3,
|
| 101 |
-
"range": [50, 220],
|
| 102 |
-
"quality_percentage": 75
|
| 103 |
-
}
|
| 104 |
-
},
|
| 105 |
-
"exposure": {
|
| 106 |
-
"status": "PASS|FAIL",
|
| 107 |
-
"score": 90.0,
|
| 108 |
-
"weight": 15,
|
| 109 |
-
"message": "Exposure and dynamic range are good",
|
| 110 |
-
"details": {
|
| 111 |
-
"dynamic_range": 128,
|
| 112 |
-
"clipping_percentage": 0.5,
|
| 113 |
-
"max_clipping_allowed": 2
|
| 114 |
-
}
|
| 115 |
-
},
|
| 116 |
-
"metadata": {
|
| 117 |
-
"status": "PASS|FAIL",
|
| 118 |
-
"score": 60.0,
|
| 119 |
-
"weight": 15,
|
| 120 |
-
"message": "Sufficient metadata extracted",
|
| 121 |
-
"details": {
|
| 122 |
-
"completeness": 45,
|
| 123 |
-
"required": 15,
|
| 124 |
-
"extracted_fields": ["timestamp", "camera_make_model", "iso"]
|
| 125 |
-
}
|
| 126 |
-
}
|
| 127 |
-
},
|
| 128 |
-
"recommendations": [
|
| 129 |
-
"Consider reducing brightness slightly for optimal quality",
|
| 130 |
-
"Image is suitable for civic documentation"
|
| 131 |
-
]
|
| 132 |
-
},
|
| 133 |
-
"message": "Image validation completed successfully",
|
| 134 |
-
"error": null
|
| 135 |
-
}
|
| 136 |
-
```
|
| 137 |
-
|
| 138 |
-
**Scoring System:**
|
| 139 |
-
- **Overall Score**: Weighted average of all validation checks
|
| 140 |
-
- **Pass Threshold**: 65% overall score required
|
| 141 |
-
- **Component Weights**:
|
| 142 |
-
- Blur Detection: 25%
|
| 143 |
-
- Resolution Check: 25%
|
| 144 |
-
- Brightness Validation: 20%
|
| 145 |
-
- Exposure Analysis: 15%
|
| 146 |
-
- Metadata Extraction: 15%
|
| 147 |
-
|
| 148 |
-
**Example:**
|
| 149 |
-
```bash
|
| 150 |
-
curl -X POST -F 'image=@test_photo.jpg' http://localhost:5000/api/validate
|
| 151 |
-
```
|
| 152 |
-
|
| 153 |
-
---
|
| 154 |
-
|
| 155 |
-
### 3. Processing Statistics
|
| 156 |
-
|
| 157 |
-
**Endpoint**: `GET /api/summary`
|
| 158 |
-
**Purpose**: System performance metrics and acceptance rates
|
| 159 |
-
|
| 160 |
-
**Response:**
|
| 161 |
-
```json
|
| 162 |
-
{
|
| 163 |
-
"success": true,
|
| 164 |
-
"data": {
|
| 165 |
-
"total_processed": 156,
|
| 166 |
-
"accepted": 61,
|
| 167 |
-
"rejected": 95,
|
| 168 |
-
"acceptance_rate": 39.1,
|
| 169 |
-
"processing_stats": {
|
| 170 |
-
"avg_processing_time": 1.8,
|
| 171 |
-
"last_24_hours": {
|
| 172 |
-
"processed": 23,
|
| 173 |
-
"accepted": 9,
|
| 174 |
-
"acceptance_rate": 39.1
|
| 175 |
-
}
|
| 176 |
-
},
|
| 177 |
-
"common_rejection_reasons": [
|
| 178 |
-
"blur: 45%",
|
| 179 |
-
"resolution: 23%",
|
| 180 |
-
"brightness: 18%",
|
| 181 |
-
"exposure: 8%",
|
| 182 |
-
"metadata: 6%"
|
| 183 |
-
]
|
| 184 |
-
},
|
| 185 |
-
"message": "Processing statistics retrieved",
|
| 186 |
-
"error": null
|
| 187 |
-
}
|
| 188 |
-
```
|
| 189 |
-
|
| 190 |
-
**Example:**
|
| 191 |
-
```bash
|
| 192 |
-
curl http://localhost:5000/api/summary
|
| 193 |
-
```
|
| 194 |
-
|
| 195 |
-
---
|
| 196 |
-
|
| 197 |
-
### 4. Validation Rules
|
| 198 |
-
|
| 199 |
-
**Endpoint**: `GET /api/validation-rules`
|
| 200 |
-
**Purpose**: Current validation thresholds and requirements
|
| 201 |
-
|
| 202 |
-
**Response:**
|
| 203 |
-
```json
|
| 204 |
-
{
|
| 205 |
-
"success": true,
|
| 206 |
-
"data": {
|
| 207 |
-
"blur": {
|
| 208 |
-
"min_score": 100,
|
| 209 |
-
"metric": "variance_of_laplacian",
|
| 210 |
-
"levels": {
|
| 211 |
-
"poor": 0,
|
| 212 |
-
"acceptable": 100,
|
| 213 |
-
"excellent": 300
|
| 214 |
-
}
|
| 215 |
-
},
|
| 216 |
-
"brightness": {
|
| 217 |
-
"range": [50, 220],
|
| 218 |
-
"metric": "mean_pixel_intensity",
|
| 219 |
-
"quality_score_min": 60
|
| 220 |
-
},
|
| 221 |
-
"resolution": {
|
| 222 |
-
"min_width": 800,
|
| 223 |
-
"min_height": 600,
|
| 224 |
-
"min_megapixels": 0.5,
|
| 225 |
-
"recommended_megapixels": 2
|
| 226 |
-
},
|
| 227 |
-
"exposure": {
|
| 228 |
-
"min_score": 100,
|
| 229 |
-
"metric": "dynamic_range",
|
| 230 |
-
"acceptable_range": [80, 150],
|
| 231 |
-
"check_clipping": {
|
| 232 |
-
"max_percentage": 2
|
| 233 |
-
}
|
| 234 |
-
},
|
| 235 |
-
"metadata": {
|
| 236 |
-
"min_completeness_percentage": 15,
|
| 237 |
-
"required_fields": [
|
| 238 |
-
"timestamp",
|
| 239 |
-
"camera_make_model",
|
| 240 |
-
"orientation",
|
| 241 |
-
"iso",
|
| 242 |
-
"shutter_speed",
|
| 243 |
-
"aperture"
|
| 244 |
-
]
|
| 245 |
-
}
|
| 246 |
-
},
|
| 247 |
-
"message": "Current validation rules",
|
| 248 |
-
"error": null
|
| 249 |
-
}
|
| 250 |
-
```
|
| 251 |
-
|
| 252 |
-
**Example:**
|
| 253 |
-
```bash
|
| 254 |
-
curl http://localhost:5000/api/validation-rules
|
| 255 |
-
```
|
| 256 |
-
|
| 257 |
-
---
|
| 258 |
-
|
| 259 |
-
### 5. API Information
|
| 260 |
-
|
| 261 |
-
**Endpoint**: `GET /api/test-api`
|
| 262 |
-
**Purpose**: API capabilities and endpoint documentation
|
| 263 |
-
|
| 264 |
-
**Response:**
|
| 265 |
-
```json
|
| 266 |
-
{
|
| 267 |
-
"success": true,
|
| 268 |
-
"data": {
|
| 269 |
-
"api_version": "2.0",
|
| 270 |
-
"endpoints": {
|
| 271 |
-
"GET /api/health": "Health check",
|
| 272 |
-
"POST /api/validate": "Main validation endpoint",
|
| 273 |
-
"GET /api/summary": "Processing statistics",
|
| 274 |
-
"GET /api/validation-rules": "Get current validation rules",
|
| 275 |
-
"GET /api/test-api": "This test endpoint",
|
| 276 |
-
"POST /api/upload": "Legacy upload endpoint"
|
| 277 |
-
},
|
| 278 |
-
"features": [
|
| 279 |
-
"Mobile-optimized validation",
|
| 280 |
-
"Weighted scoring system",
|
| 281 |
-
"Partial credit evaluation",
|
| 282 |
-
"Real-time processing",
|
| 283 |
-
"Comprehensive feedback"
|
| 284 |
-
]
|
| 285 |
-
},
|
| 286 |
-
"message": "API information retrieved",
|
| 287 |
-
"error": null
|
| 288 |
-
}
|
| 289 |
-
```
|
| 290 |
-
|
| 291 |
-
---
|
| 292 |
-
|
| 293 |
-
### 6. Legacy Upload (Deprecated)
|
| 294 |
-
|
| 295 |
-
**Endpoint**: `POST /api/upload`
|
| 296 |
-
**Purpose**: Legacy endpoint for backward compatibility
|
| 297 |
-
**Status**: ⚠️ **Deprecated** - Use `/api/validate` instead
|
| 298 |
-
|
| 299 |
-
---
|
| 300 |
-
|
| 301 |
-
## 📊 Validation Components
|
| 302 |
-
|
| 303 |
-
### Blur Detection (25% Weight)
|
| 304 |
-
- **Method**: Laplacian variance analysis
|
| 305 |
-
- **Threshold**: 100 (mobile-optimized)
|
| 306 |
-
- **Levels**: Poor (0-99), Acceptable (100-299), Excellent (300+)
|
| 307 |
-
|
| 308 |
-
### Resolution Check (25% Weight)
|
| 309 |
-
- **Minimum**: 800×600 pixels (0.5 megapixels)
|
| 310 |
-
- **Recommended**: 2+ megapixels
|
| 311 |
-
- **Mobile-Friendly**: Optimized for smartphone cameras
|
| 312 |
-
|
| 313 |
-
### Brightness Validation (20% Weight)
|
| 314 |
-
- **Range**: 50-220 pixel intensity
|
| 315 |
-
- **Method**: Histogram analysis
|
| 316 |
-
- **Quality Threshold**: 60% minimum
|
| 317 |
-
|
| 318 |
-
### Exposure Analysis (15% Weight)
|
| 319 |
-
- **Dynamic Range**: 80-150 acceptable
|
| 320 |
-
- **Clipping Check**: Max 2% clipped pixels
|
| 321 |
-
- **Method**: Pixel value distribution analysis
|
| 322 |
-
|
| 323 |
-
### Metadata Extraction (15% Weight)
|
| 324 |
-
- **Required Completeness**: 15% (mobile-friendly)
|
| 325 |
-
- **Key Fields**: Timestamp, camera info, settings
|
| 326 |
-
- **EXIF Analysis**: Automatic extraction and validation
|
| 327 |
-
|
| 328 |
-
---
|
| 329 |
-
|
| 330 |
-
## 🚨 Error Handling
|
| 331 |
-
|
| 332 |
-
### Standard Error Response
|
| 333 |
-
```json
|
| 334 |
-
{
|
| 335 |
-
"success": false,
|
| 336 |
-
"data": null,
|
| 337 |
-
"message": "Error description",
|
| 338 |
-
"error": {
|
| 339 |
-
"code": "ERROR_CODE",
|
| 340 |
-
"details": "Detailed error information"
|
| 341 |
-
}
|
| 342 |
-
}
|
| 343 |
-
```
|
| 344 |
-
|
| 345 |
-
### Common Error Codes
|
| 346 |
-
- `INVALID_IMAGE`: Image format not supported or corrupted
|
| 347 |
-
- `FILE_TOO_LARGE`: Image exceeds size limit (32MB)
|
| 348 |
-
- `PROCESSING_ERROR`: Internal validation error
|
| 349 |
-
- `MISSING_IMAGE`: No image provided in request
|
| 350 |
-
- `SERVER_ERROR`: Internal server error
|
| 351 |
-
|
| 352 |
-
---
|
| 353 |
-
|
| 354 |
-
## 🔧 Usage Examples
|
| 355 |
-
|
| 356 |
-
### JavaScript/Fetch
|
| 357 |
-
```javascript
|
| 358 |
-
const formData = new FormData();
|
| 359 |
-
formData.append('image', imageFile);
|
| 360 |
-
|
| 361 |
-
fetch('/api/validate', {
|
| 362 |
-
method: 'POST',
|
| 363 |
-
body: formData
|
| 364 |
-
})
|
| 365 |
-
.then(response => response.json())
|
| 366 |
-
.then(data => {
|
| 367 |
-
console.log('Validation result:', data);
|
| 368 |
-
if (data.success && data.data.summary.overall_status === 'PASS') {
|
| 369 |
-
console.log('Image accepted with score:', data.data.summary.overall_score);
|
| 370 |
-
}
|
| 371 |
-
});
|
| 372 |
-
```
|
| 373 |
-
|
| 374 |
-
### Python/Requests
|
| 375 |
-
```python
|
| 376 |
-
import requests
|
| 377 |
-
|
| 378 |
-
with open('image.jpg', 'rb') as f:
|
| 379 |
-
files = {'image': f}
|
| 380 |
-
response = requests.post('http://localhost:5000/api/validate', files=files)
|
| 381 |
-
|
| 382 |
-
result = response.json()
|
| 383 |
-
if result['success'] and result['data']['summary']['overall_status'] == 'PASS':
|
| 384 |
-
print(f"Image accepted with score: {result['data']['summary']['overall_score']}")
|
| 385 |
-
```
|
| 386 |
-
|
| 387 |
-
### cURL Examples
|
| 388 |
-
```bash
|
| 389 |
-
# Validate image
|
| 390 |
-
curl -X POST -F 'image=@photo.jpg' http://localhost:5000/api/validate
|
| 391 |
-
|
| 392 |
-
# Check system health
|
| 393 |
-
curl http://localhost:5000/api/health
|
| 394 |
-
|
| 395 |
-
# Get processing statistics
|
| 396 |
-
curl http://localhost:5000/api/summary
|
| 397 |
-
|
| 398 |
-
# View validation rules
|
| 399 |
-
curl http://localhost:5000/api/validation-rules
|
| 400 |
-
```
|
| 401 |
-
|
| 402 |
-
---
|
| 403 |
-
|
| 404 |
-
## 📈 Performance Characteristics
|
| 405 |
-
|
| 406 |
-
- **Processing Time**: <2 seconds per image
|
| 407 |
-
- **Concurrent Requests**: Supports multiple simultaneous validations
|
| 408 |
-
- **Memory Usage**: Optimized for mobile image sizes
|
| 409 |
-
- **Acceptance Rate**: 35-40% for quality mobile photos
|
| 410 |
-
- **Supported Formats**: JPG, JPEG, PNG, HEIC, WebP
|
| 411 |
-
- **Maximum File Size**: 32MB
|
| 412 |
-
|
| 413 |
-
---
|
| 414 |
-
|
| 415 |
-
## 🔒 Security Considerations
|
| 416 |
-
|
| 417 |
-
- **File Type Validation**: Only image formats accepted
|
| 418 |
-
- **Size Limits**: 32MB maximum file size
|
| 419 |
-
- **Input Sanitization**: All uploads validated and sanitized
|
| 420 |
-
- **Temporary Storage**: Images automatically cleaned up
|
| 421 |
-
- **No Data Persistence**: Original images not permanently stored
|
| 422 |
-
|
| 423 |
-
---
|
| 424 |
-
|
| 425 |
-
**Documentation Version**: 2.0
|
| 426 |
-
**API Version**: 2.0
|
| 427 |
-
**Last Updated**: September 25, 2025
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/DEPLOYMENT.md
DELETED
|
@@ -1,606 +0,0 @@
|
|
| 1 |
-
# Production Deployment Guide
|
| 2 |
-
|
| 3 |
-
**Version**: 2.0
|
| 4 |
-
**Status**: ✅ **Production Ready**
|
| 5 |
-
**Last Updated**: September 25, 2025
|
| 6 |
-
|
| 7 |
-
## 🎯 Overview
|
| 8 |
-
|
| 9 |
-
This guide covers deploying the **Civic Quality Control API v2.0** - a production-ready mobile photo validation system with weighted scoring and optimized acceptance rates for civic documentation.
|
| 10 |
-
|
| 11 |
-
## 🚀 Key Production Features
|
| 12 |
-
|
| 13 |
-
### **Advanced Validation System**
|
| 14 |
-
- ⚖️ **Weighted Scoring**: Intelligent partial credit system (65% pass threshold)
|
| 15 |
-
- 📱 **Mobile-Optimized**: Realistic thresholds for smartphone photography
|
| 16 |
-
- 🎯 **High Acceptance Rate**: 35-40% acceptance rate for quality mobile photos
|
| 17 |
-
- ⚡ **Fast Processing**: <2 seconds per image validation
|
| 18 |
-
- 📊 **Comprehensive API**: 6 endpoints with detailed feedback
|
| 19 |
-
|
| 20 |
-
### **Validation Components**
|
| 21 |
-
- 🔍 **Blur Detection** (25% weight) - Laplacian variance ≥100
|
| 22 |
-
- 📐 **Resolution Check** (25% weight) - Min 800×600px, 0.5MP
|
| 23 |
-
- 💡 **Brightness Validation** (20% weight) - Range 50-220 intensity
|
| 24 |
-
- 🌅 **Exposure Analysis** (15% weight) - Dynamic range + clipping check
|
| 25 |
-
- 📋 **Metadata Extraction** (15% weight) - 15% EXIF completeness required
|
| 26 |
-
|
| 27 |
-
---
|
| 28 |
-
|
| 29 |
-
## 🏗️ Quick Start
|
| 30 |
-
|
| 31 |
-
### 1. Prerequisites Check
|
| 32 |
-
|
| 33 |
-
```bash
|
| 34 |
-
# Verify Python version
|
| 35 |
-
python --version # Required: 3.8+
|
| 36 |
-
|
| 37 |
-
# Check system resources
|
| 38 |
-
# RAM: 2GB+ recommended
|
| 39 |
-
# Storage: 1GB+ for models and processing
|
| 40 |
-
# CPU: 2+ cores recommended
|
| 41 |
-
```
|
| 42 |
-
|
| 43 |
-
### 2. Local Development Setup
|
| 44 |
-
|
| 45 |
-
```bash
|
| 46 |
-
# Clone and navigate to project
|
| 47 |
-
cd civic_quality_app
|
| 48 |
-
|
| 49 |
-
# Install dependencies
|
| 50 |
-
pip install -r requirements.txt
|
| 51 |
-
|
| 52 |
-
# Setup directories and download models
|
| 53 |
-
python scripts/setup_directories.py
|
| 54 |
-
python scripts/download_models.py
|
| 55 |
-
|
| 56 |
-
# Start development server
|
| 57 |
-
python app.py
|
| 58 |
-
|
| 59 |
-
# Test the API
|
| 60 |
-
curl http://localhost:5000/api/health
|
| 61 |
-
```
|
| 62 |
-
|
| 63 |
-
**Access Points:**
|
| 64 |
-
- **API Base**: `http://localhost:5000/api/`
|
| 65 |
-
- **Mobile Interface**: `http://localhost:5000/mobile_upload.html`
|
| 66 |
-
- **Health Check**: `http://localhost:5000/api/health`
|
| 67 |
-
|
| 68 |
-
### 3. Production Deployment Options
|
| 69 |
-
|
| 70 |
-
#### **Option A: Docker (Recommended)**
|
| 71 |
-
|
| 72 |
-
```bash
|
| 73 |
-
# Build production image
|
| 74 |
-
docker build -t civic-quality-app:v2.0 .
|
| 75 |
-
|
| 76 |
-
# Run with production settings
|
| 77 |
-
docker run -d \
|
| 78 |
-
--name civic-quality-prod \
|
| 79 |
-
-p 8000:8000 \
|
| 80 |
-
-e SECRET_KEY=your-production-secret-key-here \
|
| 81 |
-
-e FLASK_ENV=production \
|
| 82 |
-
-v $(pwd)/storage:/app/storage \
|
| 83 |
-
-v $(pwd)/logs:/app/logs \
|
| 84 |
-
--restart unless-stopped \
|
| 85 |
-
civic-quality-app:v2.0
|
| 86 |
-
|
| 87 |
-
# Or use Docker Compose
|
| 88 |
-
docker-compose up -d
|
| 89 |
-
```
|
| 90 |
-
|
| 91 |
-
#### **Option B: Manual Production**
|
| 92 |
-
|
| 93 |
-
```bash
|
| 94 |
-
# Install production server
|
| 95 |
-
pip install gunicorn
|
| 96 |
-
|
| 97 |
-
# Run with Gunicorn (4 workers)
|
| 98 |
-
gunicorn --bind 0.0.0.0:8000 \
|
| 99 |
-
--workers 4 \
|
| 100 |
-
--timeout 120 \
|
| 101 |
-
--max-requests 1000 \
|
| 102 |
-
--max-requests-jitter 100 \
|
| 103 |
-
production:app
|
| 104 |
-
|
| 105 |
-
# Or use provided script
|
| 106 |
-
chmod +x start_production.sh
|
| 107 |
-
./start_production.sh
|
| 108 |
-
```
|
| 109 |
-
|
| 110 |
-
#### **Option C: Cloud Deployment**
|
| 111 |
-
|
| 112 |
-
**AWS/Azure/GCP:**
|
| 113 |
-
```bash
|
| 114 |
-
# Use production Docker image
|
| 115 |
-
# Configure load balancer for port 8000
|
| 116 |
-
# Set environment variables via cloud console
|
| 117 |
-
# Enable auto-scaling based on CPU/memory
|
| 118 |
-
```
|
| 119 |
-
|
| 120 |
-
---
|
| 121 |
-
|
| 122 |
-
## ⚙️ Production Configuration
|
| 123 |
-
|
| 124 |
-
### **Environment Variables**
|
| 125 |
-
|
| 126 |
-
```bash
|
| 127 |
-
# === Core Application ===
|
| 128 |
-
SECRET_KEY=your-256-bit-production-secret-key
|
| 129 |
-
FLASK_ENV=production
|
| 130 |
-
DEBUG=False
|
| 131 |
-
|
| 132 |
-
# === File Handling ===
|
| 133 |
-
MAX_CONTENT_LENGTH=33554432 # 32MB max file size
|
| 134 |
-
UPLOAD_FOLDER=storage/temp
|
| 135 |
-
PROCESSED_FOLDER=storage/processed
|
| 136 |
-
REJECTED_FOLDER=storage/rejected
|
| 137 |
-
|
| 138 |
-
# === Validation Thresholds (Mobile-Optimized) ===
|
| 139 |
-
BLUR_THRESHOLD=100 # Laplacian variance minimum
|
| 140 |
-
MIN_BRIGHTNESS=50 # Minimum pixel intensity
|
| 141 |
-
MAX_BRIGHTNESS=220 # Maximum pixel intensity
|
| 142 |
-
MIN_RESOLUTION_WIDTH=800 # Minimum width pixels
|
| 143 |
-
MIN_RESOLUTION_HEIGHT=600 # Minimum height pixels
|
| 144 |
-
MIN_MEGAPIXELS=0.5 # Minimum megapixels
|
| 145 |
-
METADATA_COMPLETENESS=15 # Required EXIF completeness %
|
| 146 |
-
|
| 147 |
-
# === Performance ===
|
| 148 |
-
WORKERS=4 # Gunicorn workers
|
| 149 |
-
MAX_REQUESTS=1000 # Requests per worker
|
| 150 |
-
TIMEOUT=120 # Request timeout seconds
|
| 151 |
-
|
| 152 |
-
# === Security ===
|
| 153 |
-
ALLOWED_EXTENSIONS=jpg,jpeg,png,heic,webp
|
| 154 |
-
SECURE_HEADERS=True
|
| 155 |
-
```
|
| 156 |
-
|
| 157 |
-
### **Production Configuration File**
|
| 158 |
-
|
| 159 |
-
Create `production_config.py`:
|
| 160 |
-
```python
|
| 161 |
-
import os
|
| 162 |
-
from config import VALIDATION_RULES
|
| 163 |
-
|
| 164 |
-
class ProductionConfig:
|
| 165 |
-
SECRET_KEY = os.environ.get('SECRET_KEY') or 'fallback-key-change-in-production'
|
| 166 |
-
MAX_CONTENT_LENGTH = 32 * 1024 * 1024 # 32MB
|
| 167 |
-
|
| 168 |
-
# Optimized validation rules
|
| 169 |
-
VALIDATION_RULES = VALIDATION_RULES
|
| 170 |
-
|
| 171 |
-
# Performance settings
|
| 172 |
-
SEND_FILE_MAX_AGE_DEFAULT = 31536000 # 1 year cache
|
| 173 |
-
PROPAGATE_EXCEPTIONS = True
|
| 174 |
-
|
| 175 |
-
# Security
|
| 176 |
-
SESSION_COOKIE_SECURE = True
|
| 177 |
-
SESSION_COOKIE_HTTPONLY = True
|
| 178 |
-
WTF_CSRF_ENABLED = True
|
| 179 |
-
```
|
| 180 |
-
|
| 181 |
-
---
|
| 182 |
-
|
| 183 |
-
## 🏗️ Production Architecture
|
| 184 |
-
|
| 185 |
-
### **System Components**
|
| 186 |
-
|
| 187 |
-
```
|
| 188 |
-
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
| 189 |
-
│ Load Balancer │────│ Civic Quality │────│ File Storage │
|
| 190 |
-
│ (nginx/ALB) │ │ API │ │ (persistent) │
|
| 191 |
-
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
| 192 |
-
│
|
| 193 |
-
┌─────────────────┐
|
| 194 |
-
│ ML Models │
|
| 195 |
-
│ (YOLOv8) │
|
| 196 |
-
└─────────────────┘
|
| 197 |
-
```
|
| 198 |
-
|
| 199 |
-
### **Nginx Configuration** (Optional Reverse Proxy)
|
| 200 |
-
|
| 201 |
-
```nginx
|
| 202 |
-
server {
|
| 203 |
-
listen 80;
|
| 204 |
-
server_name your-domain.com;
|
| 205 |
-
|
| 206 |
-
client_max_body_size 32M;
|
| 207 |
-
|
| 208 |
-
location / {
|
| 209 |
-
proxy_pass http://127.0.0.1:8000;
|
| 210 |
-
proxy_set_header Host $host;
|
| 211 |
-
proxy_set_header X-Real-IP $remote_addr;
|
| 212 |
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 213 |
-
proxy_connect_timeout 120s;
|
| 214 |
-
proxy_read_timeout 120s;
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
# Static files (if serving directly)
|
| 218 |
-
location /static/ {
|
| 219 |
-
alias /app/static/;
|
| 220 |
-
expires 1y;
|
| 221 |
-
add_header Cache-Control "public, immutable";
|
| 222 |
-
}
|
| 223 |
-
}
|
| 224 |
-
```
|
| 225 |
-
|
| 226 |
-
---
|
| 227 |
-
|
| 228 |
-
## 📊 Performance & Monitoring
|
| 229 |
-
|
| 230 |
-
### **Key Metrics to Monitor**
|
| 231 |
-
|
| 232 |
-
```bash
|
| 233 |
-
# Application Health
|
| 234 |
-
curl http://your-domain.com/api/health
|
| 235 |
-
|
| 236 |
-
# Processing Statistics
|
| 237 |
-
curl http://your-domain.com/api/summary
|
| 238 |
-
|
| 239 |
-
# Response Time Monitoring
|
| 240 |
-
curl -w "@curl-format.txt" -o /dev/null -s http://your-domain.com/api/health
|
| 241 |
-
```
|
| 242 |
-
|
| 243 |
-
### **Expected Performance**
|
| 244 |
-
|
| 245 |
-
- **Processing Time**: 1-3 seconds per image
|
| 246 |
-
- **Acceptance Rate**: 35-40% for mobile photos
|
| 247 |
-
- **Throughput**: 100+ images/minute (4 workers)
|
| 248 |
-
- **Memory Usage**: ~200MB per worker
|
| 249 |
-
- **CPU Usage**: 50-80% during processing
|
| 250 |
-
|
| 251 |
-
### **Monitoring Setup**
|
| 252 |
-
|
| 253 |
-
```bash
|
| 254 |
-
# Application logs
|
| 255 |
-
tail -f logs/app.log
|
| 256 |
-
|
| 257 |
-
# System monitoring
|
| 258 |
-
htop
|
| 259 |
-
df -h # Check disk space
|
| 260 |
-
```
|
| 261 |
-
|
| 262 |
-
---
|
| 263 |
-
|
| 264 |
-
## 🧪 Production Testing
|
| 265 |
-
|
| 266 |
-
### **Pre-Deployment Testing**
|
| 267 |
-
|
| 268 |
-
```bash
|
| 269 |
-
# 1. Run comprehensive API tests
|
| 270 |
-
python api_test.py
|
| 271 |
-
|
| 272 |
-
# 2. Test production server locally
|
| 273 |
-
gunicorn --bind 127.0.0.1:8000 production:app &
|
| 274 |
-
curl http://localhost:8000/api/health
|
| 275 |
-
|
| 276 |
-
# 3. Load testing (optional)
|
| 277 |
-
# Use tools like Apache Bench, wrk, or Artillery
|
| 278 |
-
ab -n 100 -c 10 http://localhost:8000/api/health
|
| 279 |
-
```
|
| 280 |
-
|
| 281 |
-
### **Post-Deployment Validation**
|
| 282 |
-
|
| 283 |
-
```bash
|
| 284 |
-
# 1. Health check
|
| 285 |
-
curl https://your-domain.com/api/health
|
| 286 |
-
|
| 287 |
-
# 2. Upload test image
|
| 288 |
-
curl -X POST -F 'image=@test_mobile_photo.jpg' \
|
| 289 |
-
https://your-domain.com/api/validate
|
| 290 |
-
|
| 291 |
-
# 3. Check processing statistics
|
| 292 |
-
curl https://your-domain.com/api/summary
|
| 293 |
-
|
| 294 |
-
# 4. Validate acceptance rate
|
| 295 |
-
# Should be 35-40% for realistic mobile photos
|
| 296 |
-
```
|
| 297 |
-
|
| 298 |
-
---
|
| 299 |
-
|
| 300 |
-
## 🔒 Security Considerations
|
| 301 |
-
|
| 302 |
-
### **Production Security Checklist**
|
| 303 |
-
|
| 304 |
-
- ✅ **Environment Variables**: All secrets in environment variables
|
| 305 |
-
- ✅ **File Validation**: Strict image format checking
|
| 306 |
-
- ✅ **Size Limits**: 32MB maximum file size
|
| 307 |
-
- ✅ **Input Sanitization**: All uploads validated
|
| 308 |
-
- ✅ **Temporary Cleanup**: Auto-cleanup of temp files
|
| 309 |
-
- ✅ **HTTPS**: SSL/TLS encryption in production
|
| 310 |
-
- ✅ **Rate Limiting**: Consider implementing API rate limits
|
| 311 |
-
- ✅ **Access Logs**: Monitor for suspicious activity
|
| 312 |
-
|
| 313 |
-
### **Firewall Configuration**
|
| 314 |
-
|
| 315 |
-
```bash
|
| 316 |
-
# Allow only necessary ports
|
| 317 |
-
ufw allow 22 # SSH
|
| 318 |
-
ufw allow 80 # HTTP
|
| 319 |
-
ufw allow 443 # HTTPS
|
| 320 |
-
ufw deny 5000 # Block development port
|
| 321 |
-
ufw enable
|
| 322 |
-
```
|
| 323 |
-
|
| 324 |
-
---
|
| 325 |
-
|
| 326 |
-
## 🚨 Troubleshooting
|
| 327 |
-
|
| 328 |
-
### **Common Issues & Solutions**
|
| 329 |
-
|
| 330 |
-
#### **1. Low Acceptance Rate**
|
| 331 |
-
```bash
|
| 332 |
-
# Check current rates
|
| 333 |
-
curl http://localhost:8000/api/summary
|
| 334 |
-
|
| 335 |
-
# Solution: Validation rules already optimized for mobile photos
|
| 336 |
-
# Current acceptance rate: 35-40%
|
| 337 |
-
# If still too low, adjust thresholds in config.py
|
| 338 |
-
```
|
| 339 |
-
|
| 340 |
-
#### **2. Performance Issues**
|
| 341 |
-
```bash
|
| 342 |
-
# Check processing time
|
| 343 |
-
time curl -X POST -F 'image=@test.jpg' http://localhost:8000/api/validate
|
| 344 |
-
|
| 345 |
-
# Solutions:
|
| 346 |
-
# - Increase worker count
|
| 347 |
-
# - Add more CPU/memory
|
| 348 |
-
# - Optimize image preprocessing
|
| 349 |
-
```
|
| 350 |
-
|
| 351 |
-
#### **3. Memory Issues**
|
| 352 |
-
```bash
|
| 353 |
-
# Monitor memory usage
|
| 354 |
-
free -h
|
| 355 |
-
ps aux | grep gunicorn
|
| 356 |
-
|
| 357 |
-
# Solutions:
|
| 358 |
-
# - Reduce max file size
|
| 359 |
-
# - Implement image resizing
|
| 360 |
-
# - Restart workers periodically
|
| 361 |
-
```
|
| 362 |
-
|
| 363 |
-
#### **4. File Storage Issues**
|
| 364 |
-
```bash
|
| 365 |
-
# Check disk space
|
| 366 |
-
df -h
|
| 367 |
-
|
| 368 |
-
# Clean up old files
|
| 369 |
-
find storage/temp -type f -mtime +1 -delete
|
| 370 |
-
find storage/rejected -type f -mtime +7 -delete
|
| 371 |
-
```
|
| 372 |
-
|
| 373 |
-
---
|
| 374 |
-
|
| 375 |
-
## 📈 Scaling & Optimization
|
| 376 |
-
|
| 377 |
-
### **Horizontal Scaling**
|
| 378 |
-
|
| 379 |
-
```bash
|
| 380 |
-
# Multiple server instances
|
| 381 |
-
docker run -d --name civic-quality-1 -p 8001:8000 civic-quality-app:v2.0
|
| 382 |
-
docker run -d --name civic-quality-2 -p 8002:8000 civic-quality-app:v2.0
|
| 383 |
-
|
| 384 |
-
# Load balancer configuration
|
| 385 |
-
# Route traffic across multiple instances
|
| 386 |
-
```
|
| 387 |
-
|
| 388 |
-
### **Performance Optimization**
|
| 389 |
-
|
| 390 |
-
```python
|
| 391 |
-
# config.py optimizations
|
| 392 |
-
VALIDATION_RULES = {
|
| 393 |
-
# Already optimized for mobile photography
|
| 394 |
-
# Higher thresholds = lower acceptance but better quality
|
| 395 |
-
# Lower thresholds = higher acceptance but more false positives
|
| 396 |
-
}
|
| 397 |
-
```
|
| 398 |
-
|
| 399 |
-
### **Future Enhancements**
|
| 400 |
-
|
| 401 |
-
- [ ] **Redis Caching**: Cache validation results
|
| 402 |
-
- [ ] **Background Processing**: Async image processing
|
| 403 |
-
- [ ] **CDN Integration**: Faster image delivery
|
| 404 |
-
- [ ] **Auto-scaling**: Dynamic worker adjustment
|
| 405 |
-
- [ ] **Monitoring Dashboard**: Real-time metrics
|
| 406 |
-
- [ ] **A/B Testing**: Validation rule optimization
|
| 407 |
-
|
| 408 |
-
---
|
| 409 |
-
|
| 410 |
-
## 📚 Additional Resources
|
| 411 |
-
|
| 412 |
-
### **API Documentation**
|
| 413 |
-
- **Comprehensive API Docs**: `docs/API_v2.md`
|
| 414 |
-
- **Response Format Examples**: See API documentation
|
| 415 |
-
- **Error Codes Reference**: Listed in API docs
|
| 416 |
-
|
| 417 |
-
### **Configuration Files**
|
| 418 |
-
- **Validation Rules**: `config.py`
|
| 419 |
-
- **Docker Setup**: `docker-compose.yml`
|
| 420 |
-
- **Production Server**: `production.py`
|
| 421 |
-
|
| 422 |
-
### **Testing Resources**
|
| 423 |
-
- **API Test Suite**: `api_test.py`
|
| 424 |
-
- **Individual Tests**: `test_*.py` files
|
| 425 |
-
- **Sample Images**: `tests/sample_images/`
|
| 426 |
-
|
| 427 |
-
---
|
| 428 |
-
|
| 429 |
-
**Deployment Status**: ✅ **Production Ready**
|
| 430 |
-
**API Version**: 2.0
|
| 431 |
-
**Acceptance Rate**: 35-40% (Optimized)
|
| 432 |
-
**Processing Speed**: <2 seconds per image
|
| 433 |
-
**Mobile Optimized**: ✅ Fully Compatible
|
| 434 |
-
|
| 435 |
-
```yaml
|
| 436 |
-
# docker-compose.yml
|
| 437 |
-
version: '3.8'
|
| 438 |
-
services:
|
| 439 |
-
civic-quality:
|
| 440 |
-
build: .
|
| 441 |
-
ports:
|
| 442 |
-
- "8000:8000"
|
| 443 |
-
environment:
|
| 444 |
-
- SECRET_KEY=${SECRET_KEY}
|
| 445 |
-
- FLASK_ENV=production
|
| 446 |
-
volumes:
|
| 447 |
-
- ./storage:/app/storage
|
| 448 |
-
- ./logs:/app/logs
|
| 449 |
-
restart: unless-stopped
|
| 450 |
-
|
| 451 |
-
nginx:
|
| 452 |
-
image: nginx:alpine
|
| 453 |
-
ports:
|
| 454 |
-
- "80:80"
|
| 455 |
-
- "443:443"
|
| 456 |
-
volumes:
|
| 457 |
-
- ./nginx.conf:/etc/nginx/nginx.conf
|
| 458 |
-
- ./ssl:/etc/nginx/ssl
|
| 459 |
-
depends_on:
|
| 460 |
-
- civic-quality
|
| 461 |
-
restart: unless-stopped
|
| 462 |
-
```
|
| 463 |
-
|
| 464 |
-
### Option 2: Cloud Deployment
|
| 465 |
-
|
| 466 |
-
#### Azure Container Apps
|
| 467 |
-
|
| 468 |
-
```bash
|
| 469 |
-
# Create resource group
|
| 470 |
-
az group create --name civic-quality-rg --location eastus
|
| 471 |
-
|
| 472 |
-
# Create container app environment
|
| 473 |
-
az containerapp env create \
|
| 474 |
-
--name civic-quality-env \
|
| 475 |
-
--resource-group civic-quality-rg \
|
| 476 |
-
--location eastus
|
| 477 |
-
|
| 478 |
-
# Deploy container app
|
| 479 |
-
az containerapp create \
|
| 480 |
-
--name civic-quality-app \
|
| 481 |
-
--resource-group civic-quality-rg \
|
| 482 |
-
--environment civic-quality-env \
|
| 483 |
-
--image civic-quality-app:latest \
|
| 484 |
-
--target-port 8000 \
|
| 485 |
-
--ingress external \
|
| 486 |
-
--env-vars SECRET_KEY=your-secret-key
|
| 487 |
-
```
|
| 488 |
-
|
| 489 |
-
#### AWS ECS Fargate
|
| 490 |
-
|
| 491 |
-
```json
|
| 492 |
-
{
|
| 493 |
-
"family": "civic-quality-task",
|
| 494 |
-
"networkMode": "awsvpc",
|
| 495 |
-
"requiresCompatibilities": ["FARGATE"],
|
| 496 |
-
"cpu": "512",
|
| 497 |
-
"memory": "1024",
|
| 498 |
-
"executionRoleArn": "arn:aws:iam::account:role/ecsTaskExecutionRole",
|
| 499 |
-
"containerDefinitions": [
|
| 500 |
-
{
|
| 501 |
-
"name": "civic-quality",
|
| 502 |
-
"image": "your-registry/civic-quality-app:latest",
|
| 503 |
-
"portMappings": [
|
| 504 |
-
{
|
| 505 |
-
"containerPort": 8000,
|
| 506 |
-
"protocol": "tcp"
|
| 507 |
-
}
|
| 508 |
-
],
|
| 509 |
-
"environment": [
|
| 510 |
-
{
|
| 511 |
-
"name": "SECRET_KEY",
|
| 512 |
-
"value": "your-secret-key"
|
| 513 |
-
}
|
| 514 |
-
],
|
| 515 |
-
"logConfiguration": {
|
| 516 |
-
"logDriver": "awslogs",
|
| 517 |
-
"options": {
|
| 518 |
-
"awslogs-group": "/ecs/civic-quality",
|
| 519 |
-
"awslogs-region": "us-east-1",
|
| 520 |
-
"awslogs-stream-prefix": "ecs"
|
| 521 |
-
}
|
| 522 |
-
}
|
| 523 |
-
}
|
| 524 |
-
]
|
| 525 |
-
}
|
| 526 |
-
```
|
| 527 |
-
|
| 528 |
-
## Production Considerations
|
| 529 |
-
|
| 530 |
-
### Security
|
| 531 |
-
|
| 532 |
-
1. **HTTPS**: Always use HTTPS in production
|
| 533 |
-
2. **Secret Key**: Use a strong, random secret key
|
| 534 |
-
3. **File Validation**: All uploads are validated for type and size
|
| 535 |
-
4. **CORS**: Configure CORS appropriately for your domain
|
| 536 |
-
|
| 537 |
-
### Performance
|
| 538 |
-
|
| 539 |
-
1. **Gunicorn**: Production WSGI server with multiple workers
|
| 540 |
-
2. **Model Caching**: YOLO model loaded once and cached
|
| 541 |
-
3. **File Cleanup**: Temporary files automatically cleaned up
|
| 542 |
-
4. **Optimized Processing**: Parallel processing for multiple validations
|
| 543 |
-
|
| 544 |
-
### Monitoring
|
| 545 |
-
|
| 546 |
-
1. **Health Check**: `/api/health` endpoint for load balancer
|
| 547 |
-
2. **Metrics**: Processing time and validation statistics
|
| 548 |
-
3. **Logging**: Structured logging for debugging
|
| 549 |
-
4. **Storage Monitoring**: Track processed/rejected ratios
|
| 550 |
-
|
| 551 |
-
### Scaling
|
| 552 |
-
|
| 553 |
-
1. **Horizontal**: Multiple container instances
|
| 554 |
-
2. **Load Balancer**: Distribute requests across instances
|
| 555 |
-
3. **Storage**: Use cloud storage for uploaded files
|
| 556 |
-
4. **Database**: Optional database for audit logs
|
| 557 |
-
|
| 558 |
-
## API Endpoints
|
| 559 |
-
|
| 560 |
-
- `GET /api/mobile` - Mobile upload interface
|
| 561 |
-
- `POST /api/upload` - Image upload and analysis
|
| 562 |
-
- `GET /api/health` - Health check
|
| 563 |
-
- `GET /api/summary` - Processing statistics
|
| 564 |
-
|
| 565 |
-
## Testing Production Deployment
|
| 566 |
-
|
| 567 |
-
```bash
|
| 568 |
-
# Test health endpoint
|
| 569 |
-
curl http://localhost:8000/api/health
|
| 570 |
-
|
| 571 |
-
# Test image upload (mobile interface)
|
| 572 |
-
open http://localhost:8000/api/mobile
|
| 573 |
-
|
| 574 |
-
# Test API directly
|
| 575 |
-
curl -X POST \
|
| 576 |
-
-F "image=@test_image.jpg" \
|
| 577 |
-
http://localhost:8000/api/upload
|
| 578 |
-
```
|
| 579 |
-
|
| 580 |
-
## Troubleshooting
|
| 581 |
-
|
| 582 |
-
### Common Issues
|
| 583 |
-
|
| 584 |
-
1. **Model download fails**: Check internet connectivity
|
| 585 |
-
2. **Large file uploads**: Increase `MAX_CONTENT_LENGTH`
|
| 586 |
-
3. **Permission errors**: Check file permissions on storage directories
|
| 587 |
-
4. **Memory issues**: Increase container memory allocation
|
| 588 |
-
|
| 589 |
-
### Logs
|
| 590 |
-
|
| 591 |
-
```bash
|
| 592 |
-
# View container logs
|
| 593 |
-
docker logs civic-quality
|
| 594 |
-
|
| 595 |
-
# View application logs
|
| 596 |
-
tail -f logs/app.log
|
| 597 |
-
```
|
| 598 |
-
|
| 599 |
-
## Support
|
| 600 |
-
|
| 601 |
-
For issues and support:
|
| 602 |
-
|
| 603 |
-
1. Check the logs for error details
|
| 604 |
-
2. Verify configuration settings
|
| 605 |
-
3. Test with sample images
|
| 606 |
-
4. Review the troubleshooting section
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/DEPLOYMENT_CHECKLIST.md
DELETED
|
@@ -1,351 +0,0 @@
|
|
| 1 |
-
# Production Deployment Checklist
|
| 2 |
-
|
| 3 |
-
**Civic Quality Control API v2.0**
|
| 4 |
-
**Date**: September 25, 2025
|
| 5 |
-
**Status**: ✅ Production Ready
|
| 6 |
-
|
| 7 |
-
---
|
| 8 |
-
|
| 9 |
-
## 🎯 Pre-Deployment Verification
|
| 10 |
-
|
| 11 |
-
### **✅ System Requirements Met**
|
| 12 |
-
- [x] Python 3.8+ installed
|
| 13 |
-
- [x] 2GB+ RAM available
|
| 14 |
-
- [x] 1GB+ storage space
|
| 15 |
-
- [x] 2+ CPU cores (recommended)
|
| 16 |
-
- [x] Network connectivity for model downloads
|
| 17 |
-
|
| 18 |
-
### **✅ Core Functionality Validated**
|
| 19 |
-
- [x] **API Health**: All 6 endpoints functional
|
| 20 |
-
- [x] **Validation Pipeline**: Weighted scoring system working
|
| 21 |
-
- [x] **Mobile Optimization**: Realistic thresholds implemented
|
| 22 |
-
- [x] **Acceptance Rate**: 35-40% achieved (improved from 16.67%)
|
| 23 |
-
- [x] **Response Format**: New structured JSON format implemented
|
| 24 |
-
- [x] **Performance**: <2 second processing time per image
|
| 25 |
-
|
| 26 |
-
### **✅ Configuration Optimized**
|
| 27 |
-
- [x] **Validation Rules**: Mobile-friendly thresholds set
|
| 28 |
-
- Blur threshold: 100 (Laplacian variance)
|
| 29 |
-
- Brightness range: 50-220 (pixel intensity)
|
| 30 |
-
- Resolution minimum: 800×600 pixels (0.5MP)
|
| 31 |
-
- Metadata requirement: 15% completeness
|
| 32 |
-
- Exposure range: 80-150 dynamic range
|
| 33 |
-
- [x] **Weighted Scoring**: Partial credit system (65% pass threshold)
|
| 34 |
-
- [x] **File Handling**: 32MB max size, proper format validation
|
| 35 |
-
|
| 36 |
-
---
|
| 37 |
-
|
| 38 |
-
## 🔧 Deployment Options
|
| 39 |
-
|
| 40 |
-
### **Option 1: Docker Deployment (Recommended)**
|
| 41 |
-
|
| 42 |
-
#### **Pre-deployment Steps:**
|
| 43 |
-
```bash
|
| 44 |
-
# 1. Verify Docker installation
|
| 45 |
-
docker --version
|
| 46 |
-
docker-compose --version
|
| 47 |
-
|
| 48 |
-
# 2. Build production image
|
| 49 |
-
docker build -t civic-quality-app:v2.0 .
|
| 50 |
-
|
| 51 |
-
# 3. Test locally first
|
| 52 |
-
docker run -p 8000:8000 civic-quality-app:v2.0
|
| 53 |
-
curl http://localhost:8000/api/health
|
| 54 |
-
```
|
| 55 |
-
|
| 56 |
-
#### **Production Deployment:**
|
| 57 |
-
```bash
|
| 58 |
-
# Set production environment variables
|
| 59 |
-
export SECRET_KEY="your-256-bit-production-secret-key"
|
| 60 |
-
export FLASK_ENV="production"
|
| 61 |
-
|
| 62 |
-
# Deploy with Docker Compose
|
| 63 |
-
docker-compose up -d
|
| 64 |
-
|
| 65 |
-
# Verify deployment
|
| 66 |
-
docker-compose ps
|
| 67 |
-
docker-compose logs civic-quality-app
|
| 68 |
-
```
|
| 69 |
-
|
| 70 |
-
#### **Post-deployment Validation:**
|
| 71 |
-
```bash
|
| 72 |
-
# Health check
|
| 73 |
-
curl http://your-domain:8000/api/health
|
| 74 |
-
|
| 75 |
-
# Test image validation
|
| 76 |
-
curl -X POST -F 'image=@test_mobile_photo.jpg' \
|
| 77 |
-
http://your-domain:8000/api/validate
|
| 78 |
-
|
| 79 |
-
# Check statistics
|
| 80 |
-
curl http://your-domain:8000/api/summary
|
| 81 |
-
```
|
| 82 |
-
|
| 83 |
-
---
|
| 84 |
-
|
| 85 |
-
### **Option 2: Manual Production Server**
|
| 86 |
-
|
| 87 |
-
#### **Server Setup:**
|
| 88 |
-
```bash
|
| 89 |
-
# 1. Install production dependencies
|
| 90 |
-
pip install -r requirements.txt gunicorn
|
| 91 |
-
|
| 92 |
-
# 2. Setup directories
|
| 93 |
-
python scripts/setup_directories.py
|
| 94 |
-
python scripts/download_models.py
|
| 95 |
-
|
| 96 |
-
# 3. Configure environment
|
| 97 |
-
export SECRET_KEY="your-production-secret-key"
|
| 98 |
-
export FLASK_ENV="production"
|
| 99 |
-
export MAX_CONTENT_LENGTH="33554432"
|
| 100 |
-
```
|
| 101 |
-
|
| 102 |
-
#### **Start Production Server:**
|
| 103 |
-
```bash
|
| 104 |
-
# Using Gunicorn (recommended)
|
| 105 |
-
gunicorn --bind 0.0.0.0:8000 \
|
| 106 |
-
--workers 4 \
|
| 107 |
-
--timeout 120 \
|
| 108 |
-
--max-requests 1000 \
|
| 109 |
-
production:app
|
| 110 |
-
|
| 111 |
-
# Or use provided script
|
| 112 |
-
chmod +x start_production.sh
|
| 113 |
-
./start_production.sh
|
| 114 |
-
```
|
| 115 |
-
|
| 116 |
-
---
|
| 117 |
-
|
| 118 |
-
## 📊 Post-Deployment Testing
|
| 119 |
-
|
| 120 |
-
### **✅ Comprehensive API Testing**
|
| 121 |
-
|
| 122 |
-
```bash
|
| 123 |
-
# Run full test suite
|
| 124 |
-
python api_test.py
|
| 125 |
-
|
| 126 |
-
# Expected results:
|
| 127 |
-
# - 5/5 tests passed
|
| 128 |
-
# - All endpoints responding correctly
|
| 129 |
-
# - Acceptance rate: 35-40%
|
| 130 |
-
# - Processing time: <2 seconds
|
| 131 |
-
```
|
| 132 |
-
|
| 133 |
-
### **✅ Load Testing (Optional)**
|
| 134 |
-
|
| 135 |
-
```bash
|
| 136 |
-
# Simple load test
|
| 137 |
-
ab -n 100 -c 10 http://your-domain:8000/api/health
|
| 138 |
-
|
| 139 |
-
# Image validation load test
|
| 140 |
-
for i in {1..10}; do
|
| 141 |
-
curl -X POST -F 'image=@test_image.jpg' \
|
| 142 |
-
http://your-domain:8000/api/validate &
|
| 143 |
-
done
|
| 144 |
-
wait
|
| 145 |
-
```
|
| 146 |
-
|
| 147 |
-
### **✅ Mobile Interface Testing**
|
| 148 |
-
|
| 149 |
-
1. **Access mobile interface**: `http://your-domain:8000/mobile_upload.html`
|
| 150 |
-
2. **Test camera capture**: Use device camera to take photo
|
| 151 |
-
3. **Test file upload**: Upload existing photo from gallery
|
| 152 |
-
4. **Verify validation**: Check response format and scoring
|
| 153 |
-
5. **Test various scenarios**: Different lighting, angles, quality
|
| 154 |
-
|
| 155 |
-
---
|
| 156 |
-
|
| 157 |
-
## 🔒 Security Hardening
|
| 158 |
-
|
| 159 |
-
### **✅ Production Security Checklist**
|
| 160 |
-
|
| 161 |
-
- [x] **Environment Variables**: All secrets externalized
|
| 162 |
-
- [x] **HTTPS**: SSL/TLS certificate configured (recommended)
|
| 163 |
-
- [x] **File Validation**: Strict image format checking implemented
|
| 164 |
-
- [x] **Size Limits**: 32MB maximum enforced
|
| 165 |
-
- [x] **Input Sanitization**: All uploads validated and sanitized
|
| 166 |
-
- [x] **Temporary Cleanup**: Auto-cleanup mechanisms in place
|
| 167 |
-
- [x] **Error Handling**: No sensitive information in error responses
|
| 168 |
-
|
| 169 |
-
### **✅ Firewall Configuration**
|
| 170 |
-
|
| 171 |
-
```bash
|
| 172 |
-
# Recommended firewall rules
|
| 173 |
-
ufw allow 22 # SSH access
|
| 174 |
-
ufw allow 80 # HTTP
|
| 175 |
-
ufw allow 443 # HTTPS
|
| 176 |
-
ufw allow 8000 # API port (or use nginx proxy)
|
| 177 |
-
ufw deny 5000 # Block development port
|
| 178 |
-
ufw enable
|
| 179 |
-
```
|
| 180 |
-
|
| 181 |
-
---
|
| 182 |
-
|
| 183 |
-
## 📈 Monitoring & Maintenance
|
| 184 |
-
|
| 185 |
-
### **✅ Key Metrics to Track**
|
| 186 |
-
|
| 187 |
-
1. **Application Health**
|
| 188 |
-
```bash
|
| 189 |
-
curl http://your-domain:8000/api/health
|
| 190 |
-
# Should return: "status": "healthy"
|
| 191 |
-
```
|
| 192 |
-
|
| 193 |
-
2. **Processing Statistics**
|
| 194 |
-
```bash
|
| 195 |
-
curl http://your-domain:8000/api/summary
|
| 196 |
-
# Monitor acceptance rate (target: 35-40%)
|
| 197 |
-
```
|
| 198 |
-
|
| 199 |
-
3. **Response Times**
|
| 200 |
-
```bash
|
| 201 |
-
time curl -X POST -F 'image=@test.jpg' \
|
| 202 |
-
http://your-domain:8000/api/validate
|
| 203 |
-
# Target: <2 seconds
|
| 204 |
-
```
|
| 205 |
-
|
| 206 |
-
4. **System Resources**
|
| 207 |
-
```bash
|
| 208 |
-
htop # CPU and memory usage
|
| 209 |
-
df -h # Disk space
|
| 210 |
-
du -sh storage/ # Storage usage
|
| 211 |
-
```
|
| 212 |
-
|
| 213 |
-
### **✅ Log Monitoring**
|
| 214 |
-
|
| 215 |
-
```bash
|
| 216 |
-
# Application logs
|
| 217 |
-
tail -f logs/app.log
|
| 218 |
-
|
| 219 |
-
# Docker logs (if using Docker)
|
| 220 |
-
docker-compose logs -f civic-quality-app
|
| 221 |
-
|
| 222 |
-
# System logs
|
| 223 |
-
journalctl -u civic-quality-app -f
|
| 224 |
-
```
|
| 225 |
-
|
| 226 |
-
### **✅ Maintenance Tasks**
|
| 227 |
-
|
| 228 |
-
#### **Daily:**
|
| 229 |
-
- [ ] Check application health endpoint
|
| 230 |
-
- [ ] Monitor acceptance rates
|
| 231 |
-
- [ ] Review error logs
|
| 232 |
-
|
| 233 |
-
#### **Weekly:**
|
| 234 |
-
- [ ] Clean up old temporary files
|
| 235 |
-
- [ ] Review processing statistics
|
| 236 |
-
- [ ] Check disk space usage
|
| 237 |
-
- [ ] Monitor performance metrics
|
| 238 |
-
|
| 239 |
-
#### **Monthly:**
|
| 240 |
-
- [ ] Review and optimize validation rules if needed
|
| 241 |
-
- [ ] Update dependencies (test first)
|
| 242 |
-
- [ ] Backup configuration and logs
|
| 243 |
-
- [ ] Performance optimization review
|
| 244 |
-
|
| 245 |
-
---
|
| 246 |
-
|
| 247 |
-
## 🚨 Troubleshooting Guide
|
| 248 |
-
|
| 249 |
-
### **Common Issues & Quick Fixes**
|
| 250 |
-
|
| 251 |
-
#### **1. API Not Responding**
|
| 252 |
-
```bash
|
| 253 |
-
# Check if service is running
|
| 254 |
-
curl http://localhost:8000/api/health
|
| 255 |
-
|
| 256 |
-
# Restart if needed
|
| 257 |
-
docker-compose restart civic-quality-app
|
| 258 |
-
# OR
|
| 259 |
-
pkill -f gunicorn && ./start_production.sh
|
| 260 |
-
```
|
| 261 |
-
|
| 262 |
-
#### **2. Low Acceptance Rate**
|
| 263 |
-
```bash
|
| 264 |
-
# Check current rate
|
| 265 |
-
curl http://localhost:8000/api/summary
|
| 266 |
-
|
| 267 |
-
# Current optimization: 35-40% acceptance rate
|
| 268 |
-
# Rules already optimized for mobile photography
|
| 269 |
-
# No action needed unless specific requirements change
|
| 270 |
-
```
|
| 271 |
-
|
| 272 |
-
#### **3. Slow Processing**
|
| 273 |
-
```bash
|
| 274 |
-
# Check response time
|
| 275 |
-
time curl -X POST -F 'image=@test.jpg' \
|
| 276 |
-
http://localhost:8000/api/validate
|
| 277 |
-
|
| 278 |
-
# If >3 seconds:
|
| 279 |
-
# - Check CPU usage (htop)
|
| 280 |
-
# - Consider increasing workers
|
| 281 |
-
# - Check available memory
|
| 282 |
-
```
|
| 283 |
-
|
| 284 |
-
#### **4. Storage Issues**
|
| 285 |
-
```bash
|
| 286 |
-
# Check disk space
|
| 287 |
-
df -h
|
| 288 |
-
|
| 289 |
-
# Clean old files
|
| 290 |
-
find storage/temp -type f -mtime +1 -delete
|
| 291 |
-
find storage/rejected -type f -mtime +7 -delete
|
| 292 |
-
```
|
| 293 |
-
|
| 294 |
-
---
|
| 295 |
-
|
| 296 |
-
## 📋 Success Criteria
|
| 297 |
-
|
| 298 |
-
### **✅ Deployment Successful When:**
|
| 299 |
-
|
| 300 |
-
- [x] **Health Check**: Returns "healthy" status
|
| 301 |
-
- [x] **All Endpoints**: 6 API endpoints responding correctly
|
| 302 |
-
- [x] **Validation Working**: Images processed with weighted scoring
|
| 303 |
-
- [x] **Mobile Optimized**: Realistic acceptance rates (35-40%)
|
| 304 |
-
- [x] **Performance**: <2 second processing time
|
| 305 |
-
- [x] **Response Format**: New structured JSON format
|
| 306 |
-
- [x] **Error Handling**: Graceful error responses
|
| 307 |
-
- [x] **Security**: File validation and size limits enforced
|
| 308 |
-
- [x] **Monitoring**: Logs and metrics accessible
|
| 309 |
-
|
| 310 |
-
### **✅ Production Metrics Targets**
|
| 311 |
-
|
| 312 |
-
| Metric | Target | Status |
|
| 313 |
-
|--------|--------|--------|
|
| 314 |
-
| Acceptance Rate | 35-40% | ✅ Achieved |
|
| 315 |
-
| Processing Time | <2 seconds | ✅ Achieved |
|
| 316 |
-
| API Response Time | <500ms | ✅ Achieved |
|
| 317 |
-
| Uptime | >99.9% | ✅ Ready |
|
| 318 |
-
| Error Rate | <1% | ✅ Ready |
|
| 319 |
-
|
| 320 |
-
---
|
| 321 |
-
|
| 322 |
-
## 🎉 Deployment Complete!
|
| 323 |
-
|
| 324 |
-
**Status**: ✅ **PRODUCTION READY**
|
| 325 |
-
|
| 326 |
-
Your Civic Quality Control API v2.0 is now ready for production deployment with:
|
| 327 |
-
|
| 328 |
-
- **Optimized Mobile Photography Validation**
|
| 329 |
-
- **Weighted Scoring System with Partial Credit**
|
| 330 |
-
- **35-40% Acceptance Rate (Improved from 16.67%)**
|
| 331 |
-
- **Comprehensive API with 6 Endpoints**
|
| 332 |
-
- **Production-Grade Performance & Security**
|
| 333 |
-
|
| 334 |
-
### **Next Steps:**
|
| 335 |
-
1. Deploy using your chosen method (Docker recommended)
|
| 336 |
-
2. Configure monitoring and alerting
|
| 337 |
-
3. Set up backup procedures
|
| 338 |
-
4. Document any custom configurations
|
| 339 |
-
5. Train users on the mobile interface
|
| 340 |
-
|
| 341 |
-
### **Support & Documentation:**
|
| 342 |
-
- **API Documentation**: `docs/API_v2.md`
|
| 343 |
-
- **Deployment Guide**: `docs/DEPLOYMENT.md`
|
| 344 |
-
- **Main README**: `README.md`
|
| 345 |
-
- **Test Suite**: Run `python api_test.py`
|
| 346 |
-
|
| 347 |
-
---
|
| 348 |
-
|
| 349 |
-
**Deployment Checklist Version**: 2.0
|
| 350 |
-
**Completed**: September 25, 2025
|
| 351 |
-
**Ready for Production**: ✅ YES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
models/.gitkeep
DELETED
|
File without changes
|
models/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# Models package
|
|
|
|
|
|
models/model_loader.py
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 1 |
-
import torch
|
| 2 |
-
from ultralytics import YOLO
|
| 3 |
-
|
| 4 |
-
def load_yolo_model(model_path='models/yolov8n.pt'):
|
| 5 |
-
"""
|
| 6 |
-
Load YOLOv8 model for object detection.
|
| 7 |
-
|
| 8 |
-
Args:
|
| 9 |
-
model_path (str): Path to the YOLO model file
|
| 10 |
-
|
| 11 |
-
Returns:
|
| 12 |
-
YOLO: Loaded YOLO model
|
| 13 |
-
"""
|
| 14 |
-
try:
|
| 15 |
-
model = YOLO(model_path)
|
| 16 |
-
return model
|
| 17 |
-
except Exception as e:
|
| 18 |
-
raise RuntimeError(f"Failed to load YOLO model: {e}")
|
| 19 |
-
|
| 20 |
-
# Global model instance
|
| 21 |
-
yolo_model = None
|
| 22 |
-
|
| 23 |
-
def get_yolo_model():
|
| 24 |
-
"""
|
| 25 |
-
Get the global YOLO model instance, loading it if necessary.
|
| 26 |
-
|
| 27 |
-
Returns:
|
| 28 |
-
YOLO: The YOLO model instance
|
| 29 |
-
"""
|
| 30 |
-
global yolo_model
|
| 31 |
-
if yolo_model is None:
|
| 32 |
-
yolo_model = load_yolo_model()
|
| 33 |
-
return yolo_model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nginx/nginx.conf
DELETED
|
@@ -1,89 +0,0 @@
|
|
| 1 |
-
events {
|
| 2 |
-
worker_connections 1024;
|
| 3 |
-
}
|
| 4 |
-
|
| 5 |
-
http {
|
| 6 |
-
upstream civic-quality {
|
| 7 |
-
server civic-quality:8000;
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
# Rate limiting
|
| 11 |
-
limit_req_zone $binary_remote_addr zone=upload:10m rate=10r/m;
|
| 12 |
-
|
| 13 |
-
# File upload size
|
| 14 |
-
client_max_body_size 32M;
|
| 15 |
-
|
| 16 |
-
# Timeouts
|
| 17 |
-
proxy_connect_timeout 60s;
|
| 18 |
-
proxy_send_timeout 60s;
|
| 19 |
-
proxy_read_timeout 60s;
|
| 20 |
-
|
| 21 |
-
server {
|
| 22 |
-
listen 80;
|
| 23 |
-
server_name _;
|
| 24 |
-
|
| 25 |
-
# Redirect to HTTPS (uncomment for production)
|
| 26 |
-
# return 301 https://$server_name$request_uri;
|
| 27 |
-
|
| 28 |
-
# For development, serve directly over HTTP
|
| 29 |
-
location / {
|
| 30 |
-
proxy_pass http://civic-quality;
|
| 31 |
-
proxy_set_header Host $host;
|
| 32 |
-
proxy_set_header X-Real-IP $remote_addr;
|
| 33 |
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 34 |
-
proxy_set_header X-Forwarded-Proto $scheme;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
# Rate limit upload endpoint
|
| 38 |
-
location /api/upload {
|
| 39 |
-
limit_req zone=upload burst=5 nodelay;
|
| 40 |
-
proxy_pass http://civic-quality;
|
| 41 |
-
proxy_set_header Host $host;
|
| 42 |
-
proxy_set_header X-Real-IP $remote_addr;
|
| 43 |
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 44 |
-
proxy_set_header X-Forwarded-Proto $scheme;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
# Health check endpoint
|
| 48 |
-
location /api/health {
|
| 49 |
-
proxy_pass http://civic-quality;
|
| 50 |
-
access_log off;
|
| 51 |
-
}
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
# HTTPS server (uncomment and configure for production)
|
| 55 |
-
# server {
|
| 56 |
-
# listen 443 ssl http2;
|
| 57 |
-
# server_name your-domain.com;
|
| 58 |
-
#
|
| 59 |
-
# ssl_certificate /etc/nginx/ssl/cert.pem;
|
| 60 |
-
# ssl_certificate_key /etc/nginx/ssl/key.pem;
|
| 61 |
-
#
|
| 62 |
-
# # SSL configuration
|
| 63 |
-
# ssl_protocols TLSv1.2 TLSv1.3;
|
| 64 |
-
# ssl_ciphers HIGH:!aNULL:!MD5;
|
| 65 |
-
# ssl_prefer_server_ciphers on;
|
| 66 |
-
#
|
| 67 |
-
# # Security headers
|
| 68 |
-
# add_header X-Frame-Options DENY;
|
| 69 |
-
# add_header X-Content-Type-Options nosniff;
|
| 70 |
-
# add_header X-XSS-Protection "1; mode=block";
|
| 71 |
-
#
|
| 72 |
-
# location / {
|
| 73 |
-
# proxy_pass http://civic-quality;
|
| 74 |
-
# proxy_set_header Host $host;
|
| 75 |
-
# proxy_set_header X-Real-IP $remote_addr;
|
| 76 |
-
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 77 |
-
# proxy_set_header X-Forwarded-Proto $scheme;
|
| 78 |
-
# }
|
| 79 |
-
#
|
| 80 |
-
# location /api/upload {
|
| 81 |
-
# limit_req zone=upload burst=5 nodelay;
|
| 82 |
-
# proxy_pass http://civic-quality;
|
| 83 |
-
# proxy_set_header Host $host;
|
| 84 |
-
# proxy_set_header X-Real-IP $remote_addr;
|
| 85 |
-
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 86 |
-
# proxy_set_header X-Forwarded-Proto $scheme;
|
| 87 |
-
# }
|
| 88 |
-
# }
|
| 89 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
production.py
DELETED
|
@@ -1,153 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Civic Photo Quality Control API - Production WSGI Application
|
| 4 |
-
==============================================================
|
| 5 |
-
Production-ready entry point for deployment with Gunicorn or similar WSGI servers.
|
| 6 |
-
|
| 7 |
-
Usage:
|
| 8 |
-
gunicorn --bind 0.0.0.0:8000 --workers 4 production:app
|
| 9 |
-
|
| 10 |
-
Features:
|
| 11 |
-
- Automatic directory structure setup
|
| 12 |
-
- Production logging configuration
|
| 13 |
-
- Model initialization
|
| 14 |
-
- Environment validation
|
| 15 |
-
|
| 16 |
-
Author: Civic Quality Control Team
|
| 17 |
-
Version: 2.0
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
import os
|
| 21 |
-
import sys
|
| 22 |
-
import logging
|
| 23 |
-
from pathlib import Path
|
| 24 |
-
|
| 25 |
-
# Add project root to Python path for proper module imports
|
| 26 |
-
project_root = Path(__file__).parent
|
| 27 |
-
sys.path.insert(0, str(project_root))
|
| 28 |
-
|
| 29 |
-
from app import create_app
|
| 30 |
-
from config import Config
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def setup_logging():
|
| 34 |
-
"""
|
| 35 |
-
Configure production-grade logging.
|
| 36 |
-
|
| 37 |
-
Logs are written to both console (stdout) and log file (logs/app.log).
|
| 38 |
-
Log format includes timestamp, logger name, level, and message.
|
| 39 |
-
"""
|
| 40 |
-
logging.basicConfig(
|
| 41 |
-
level=logging.INFO, # INFO level for production (change to DEBUG for troubleshooting)
|
| 42 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 43 |
-
handlers=[
|
| 44 |
-
logging.StreamHandler(sys.stdout), # Console output
|
| 45 |
-
# File output (only if logs directory exists)
|
| 46 |
-
logging.FileHandler('logs/app.log') if os.path.exists('logs') else logging.StreamHandler()
|
| 47 |
-
]
|
| 48 |
-
)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def ensure_directories():
|
| 52 |
-
"""
|
| 53 |
-
Create all required directory structures if they don't exist.
|
| 54 |
-
|
| 55 |
-
Directories created:
|
| 56 |
-
- storage/temp: Temporary upload storage
|
| 57 |
-
- storage/processed: Accepted/validated images
|
| 58 |
-
- storage/rejected: Rejected images for analysis
|
| 59 |
-
- models: Machine learning model storage
|
| 60 |
-
- logs: Application log files
|
| 61 |
-
"""
|
| 62 |
-
directories = [
|
| 63 |
-
'storage/temp',
|
| 64 |
-
'storage/processed',
|
| 65 |
-
'storage/rejected',
|
| 66 |
-
'models',
|
| 67 |
-
'logs'
|
| 68 |
-
]
|
| 69 |
-
|
| 70 |
-
for directory in directories:
|
| 71 |
-
Path(directory).mkdir(parents=True, exist_ok=True)
|
| 72 |
-
logging.info(f"Ensured directory exists: {directory}")
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def download_models():
|
| 76 |
-
"""
|
| 77 |
-
Download YOLOv8 object detection model if not already present.
|
| 78 |
-
|
| 79 |
-
The model is used for optional civic object detection feature.
|
| 80 |
-
Downloads from Ultralytics repository on first run.
|
| 81 |
-
"""
|
| 82 |
-
model_path = Path('models/yolov8n.pt')
|
| 83 |
-
if not model_path.exists():
|
| 84 |
-
try:
|
| 85 |
-
from ultralytics import YOLO
|
| 86 |
-
logging.info("YOLO model not found. Downloading...")
|
| 87 |
-
model = YOLO('yolov8n.pt') # Downloads YOLOv8n (nano) model
|
| 88 |
-
logging.info("YOLO model download completed successfully.")
|
| 89 |
-
except Exception as e:
|
| 90 |
-
logging.warning(f"Failed to download YOLO model: {e}")
|
| 91 |
-
logging.info("Object detection feature will be disabled.")
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def create_production_app():
|
| 95 |
-
"""
|
| 96 |
-
Create and configure the production Flask application.
|
| 97 |
-
|
| 98 |
-
Steps performed:
|
| 99 |
-
1. Setup logging configuration
|
| 100 |
-
2. Ensure directory structure exists
|
| 101 |
-
3. Download required models (if missing)
|
| 102 |
-
4. Create Flask app with production configuration
|
| 103 |
-
5. Validate critical configuration settings
|
| 104 |
-
|
| 105 |
-
Returns:
|
| 106 |
-
Flask application instance configured for production
|
| 107 |
-
"""
|
| 108 |
-
# Step 1: Configure logging
|
| 109 |
-
setup_logging()
|
| 110 |
-
logging.info("=" * 60)
|
| 111 |
-
logging.info("Civic Photo Quality Control API - Production Startup")
|
| 112 |
-
logging.info("=" * 60)
|
| 113 |
-
|
| 114 |
-
# Step 2: Setup directories
|
| 115 |
-
ensure_directories()
|
| 116 |
-
download_models()
|
| 117 |
-
|
| 118 |
-
# Set production environment
|
| 119 |
-
os.environ['FLASK_ENV'] = 'production'
|
| 120 |
-
|
| 121 |
-
# Create Flask app with production config
|
| 122 |
-
app = create_app('production')
|
| 123 |
-
|
| 124 |
-
# Configure for production
|
| 125 |
-
app.config.update({
|
| 126 |
-
'MAX_CONTENT_LENGTH': 32 * 1024 * 1024, # 32MB for mobile photos
|
| 127 |
-
'UPLOAD_FOLDER': 'storage/temp',
|
| 128 |
-
'PROCESSED_FOLDER': 'storage/processed',
|
| 129 |
-
'REJECTED_FOLDER': 'storage/rejected',
|
| 130 |
-
'SECRET_KEY': os.environ.get('SECRET_KEY', 'production-secret-key-change-me'),
|
| 131 |
-
'BLUR_THRESHOLD': 80.0, # More lenient for mobile
|
| 132 |
-
'MIN_BRIGHTNESS': 25,
|
| 133 |
-
'MAX_BRIGHTNESS': 235,
|
| 134 |
-
'MIN_RESOLUTION_WIDTH': 720,
|
| 135 |
-
'MIN_RESOLUTION_HEIGHT': 480,
|
| 136 |
-
})
|
| 137 |
-
|
| 138 |
-
logging.info("Civic Quality Control App started in production mode")
|
| 139 |
-
logging.info(f"Upload folder: {app.config['UPLOAD_FOLDER']}")
|
| 140 |
-
logging.info(f"Max file size: {app.config['MAX_CONTENT_LENGTH']} bytes")
|
| 141 |
-
|
| 142 |
-
return app
|
| 143 |
-
|
| 144 |
-
# Create the app instance for WSGI servers (gunicorn, uwsgi, etc.)
|
| 145 |
-
app = create_production_app()
|
| 146 |
-
|
| 147 |
-
if __name__ == '__main__':
|
| 148 |
-
# Development server (not recommended for production)
|
| 149 |
-
app.run(
|
| 150 |
-
host='0.0.0.0',
|
| 151 |
-
port=int(os.environ.get('PORT', 8000)),
|
| 152 |
-
debug=False
|
| 153 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,13 +1,16 @@
|
|
| 1 |
-
|
| 2 |
-
Flask
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
python
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
| 10 |
piexif==1.1.3
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
| 1 |
+
# Core web framework
|
| 2 |
+
Flask==3.0.3
|
| 3 |
+
Flask-CORS==4.0.1
|
| 4 |
+
gunicorn==23.0.0
|
| 5 |
+
|
| 6 |
+
# Image processing
|
| 7 |
+
opencv-python==4.10.0.84
|
| 8 |
+
Pillow==10.4.0
|
| 9 |
+
numpy==2.1.2
|
| 10 |
+
|
| 11 |
+
# Metadata & configuration
|
| 12 |
piexif==1.1.3
|
| 13 |
+
python-dotenv==1.0.1
|
| 14 |
+
|
| 15 |
+
# Development & testing
|
| 16 |
+
pytest==8.3.3
|
scripts/download_models.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Script to download YOLO models.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
import urllib.request
|
| 8 |
-
from pathlib import Path
|
| 9 |
-
|
| 10 |
-
def download_yolo_model():
|
| 11 |
-
"""Download YOLOv8 nano model if not exists."""
|
| 12 |
-
model_dir = Path("models")
|
| 13 |
-
model_dir.mkdir(exist_ok=True)
|
| 14 |
-
|
| 15 |
-
model_path = model_dir / "yolov8n.pt"
|
| 16 |
-
|
| 17 |
-
if model_path.exists():
|
| 18 |
-
print(f"Model already exists at {model_path}")
|
| 19 |
-
return
|
| 20 |
-
|
| 21 |
-
# YOLOv8n URL (example, replace with actual)
|
| 22 |
-
url = "https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n.pt"
|
| 23 |
-
|
| 24 |
-
print(f"Downloading YOLOv8n model to {model_path}...")
|
| 25 |
-
try:
|
| 26 |
-
urllib.request.urlretrieve(url, model_path)
|
| 27 |
-
print("Download complete.")
|
| 28 |
-
except Exception as e:
|
| 29 |
-
print(f"Failed to download model: {e}")
|
| 30 |
-
|
| 31 |
-
if __name__ == "__main__":
|
| 32 |
-
download_yolo_model()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/setup_directories.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Script to set up project directories.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
|
| 9 |
-
def setup_directories():
|
| 10 |
-
"""Create all necessary directories."""
|
| 11 |
-
dirs = [
|
| 12 |
-
"storage/temp",
|
| 13 |
-
"storage/processed",
|
| 14 |
-
"storage/rejected",
|
| 15 |
-
"tests/sample_images/blurry",
|
| 16 |
-
"tests/sample_images/dark",
|
| 17 |
-
"tests/sample_images/low_res",
|
| 18 |
-
"tests/sample_images/good",
|
| 19 |
-
"docs"
|
| 20 |
-
]
|
| 21 |
-
|
| 22 |
-
for dir_path in dirs:
|
| 23 |
-
Path(dir_path).mkdir(parents=True, exist_ok=True)
|
| 24 |
-
print(f"Created directory: {dir_path}")
|
| 25 |
-
|
| 26 |
-
if __name__ == "__main__":
|
| 27 |
-
setup_directories()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
start_production.bat
DELETED
|
@@ -1,74 +0,0 @@
|
|
| 1 |
-
@echo off
|
| 2 |
-
REM Production startup script for Civic Quality Control App (Windows)
|
| 3 |
-
REM This script sets up and starts the production environment
|
| 4 |
-
|
| 5 |
-
echo 🚀 Starting Civic Quality Control - Production Setup
|
| 6 |
-
echo =================================================
|
| 7 |
-
|
| 8 |
-
REM Check if Docker is installed
|
| 9 |
-
docker --version >nul 2>&1
|
| 10 |
-
if %errorlevel% neq 0 (
|
| 11 |
-
echo ❌ Docker is not installed. Please install Docker Desktop first.
|
| 12 |
-
pause
|
| 13 |
-
exit /b 1
|
| 14 |
-
)
|
| 15 |
-
|
| 16 |
-
REM Check if Docker Compose is installed
|
| 17 |
-
docker-compose --version >nul 2>&1
|
| 18 |
-
if %errorlevel% neq 0 (
|
| 19 |
-
echo ❌ Docker Compose is not installed. Please update Docker Desktop.
|
| 20 |
-
pause
|
| 21 |
-
exit /b 1
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
-
REM Create necessary directories
|
| 25 |
-
echo 📁 Creating necessary directories...
|
| 26 |
-
if not exist storage\temp mkdir storage\temp
|
| 27 |
-
if not exist storage\processed mkdir storage\processed
|
| 28 |
-
if not exist storage\rejected mkdir storage\rejected
|
| 29 |
-
if not exist logs mkdir logs
|
| 30 |
-
if not exist nginx\ssl mkdir nginx\ssl
|
| 31 |
-
|
| 32 |
-
REM Set environment variables if not already set
|
| 33 |
-
if not defined SECRET_KEY (
|
| 34 |
-
echo 🔐 Generating secret key...
|
| 35 |
-
set SECRET_KEY=change-this-in-production-windows
|
| 36 |
-
)
|
| 37 |
-
|
| 38 |
-
REM Build and start the application
|
| 39 |
-
echo 🏗️ Building and starting the application...
|
| 40 |
-
docker-compose up --build -d
|
| 41 |
-
|
| 42 |
-
REM Wait for the application to start
|
| 43 |
-
echo ⏳ Waiting for application to start...
|
| 44 |
-
timeout /t 30 /nobreak >nul
|
| 45 |
-
|
| 46 |
-
REM Test the application
|
| 47 |
-
echo 🧪 Testing the application...
|
| 48 |
-
python test_production.py --quick
|
| 49 |
-
|
| 50 |
-
REM Show status
|
| 51 |
-
echo.
|
| 52 |
-
echo 📊 Container Status:
|
| 53 |
-
docker-compose ps
|
| 54 |
-
|
| 55 |
-
echo.
|
| 56 |
-
echo 🎉 Production deployment completed!
|
| 57 |
-
echo =================================================
|
| 58 |
-
echo 📱 Mobile Interface: http://localhost/api/mobile
|
| 59 |
-
echo 🔍 Health Check: http://localhost/api/health
|
| 60 |
-
echo 📊 API Documentation: http://localhost/api/summary
|
| 61 |
-
echo.
|
| 62 |
-
echo 📋 Management Commands:
|
| 63 |
-
echo Stop: docker-compose down
|
| 64 |
-
echo Logs: docker-compose logs -f
|
| 65 |
-
echo Restart: docker-compose restart
|
| 66 |
-
echo Test: python test_production.py
|
| 67 |
-
echo.
|
| 68 |
-
echo ⚠️ For production use:
|
| 69 |
-
echo 1. Configure HTTPS with SSL certificates
|
| 70 |
-
echo 2. Set a secure SECRET_KEY environment variable
|
| 71 |
-
echo 3. Configure domain-specific CORS settings
|
| 72 |
-
echo 4. Set up monitoring and log aggregation
|
| 73 |
-
|
| 74 |
-
pause
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
start_production.sh
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
|
| 3 |
-
# Production startup script for Civic Quality Control App
|
| 4 |
-
# This script sets up and starts the production environment
|
| 5 |
-
|
| 6 |
-
set -e
|
| 7 |
-
|
| 8 |
-
echo "🚀 Starting Civic Quality Control - Production Setup"
|
| 9 |
-
echo "================================================="
|
| 10 |
-
|
| 11 |
-
# Check if Docker is installed
|
| 12 |
-
if ! command -v docker &> /dev/null; then
|
| 13 |
-
echo "❌ Docker is not installed. Please install Docker first."
|
| 14 |
-
exit 1
|
| 15 |
-
fi
|
| 16 |
-
|
| 17 |
-
# Check if Docker Compose is installed
|
| 18 |
-
if ! command -v docker-compose &> /dev/null; then
|
| 19 |
-
echo "❌ Docker Compose is not installed. Please install Docker Compose first."
|
| 20 |
-
exit 1
|
| 21 |
-
fi
|
| 22 |
-
|
| 23 |
-
# Create necessary directories
|
| 24 |
-
echo "📁 Creating necessary directories..."
|
| 25 |
-
mkdir -p storage/temp storage/processed storage/rejected logs nginx/ssl
|
| 26 |
-
|
| 27 |
-
# Set environment variables
|
| 28 |
-
export SECRET_KEY=${SECRET_KEY:-$(openssl rand -hex 32)}
|
| 29 |
-
echo "🔐 Secret key configured"
|
| 30 |
-
|
| 31 |
-
# Build and start the application
|
| 32 |
-
echo "🏗️ Building and starting the application..."
|
| 33 |
-
docker-compose up --build -d
|
| 34 |
-
|
| 35 |
-
# Wait for the application to start
|
| 36 |
-
echo "⏳ Waiting for application to start..."
|
| 37 |
-
sleep 30
|
| 38 |
-
|
| 39 |
-
# Test the application
|
| 40 |
-
echo "🧪 Testing the application..."
|
| 41 |
-
python test_production.py --quick
|
| 42 |
-
|
| 43 |
-
# Show status
|
| 44 |
-
echo ""
|
| 45 |
-
echo "📊 Container Status:"
|
| 46 |
-
docker-compose ps
|
| 47 |
-
|
| 48 |
-
echo ""
|
| 49 |
-
echo "🎉 Production deployment completed!"
|
| 50 |
-
echo "================================================="
|
| 51 |
-
echo "📱 Mobile Interface: http://localhost/api/mobile"
|
| 52 |
-
echo "🔍 Health Check: http://localhost/api/health"
|
| 53 |
-
echo "📊 API Documentation: http://localhost/api/summary"
|
| 54 |
-
echo ""
|
| 55 |
-
echo "📋 Management Commands:"
|
| 56 |
-
echo " Stop: docker-compose down"
|
| 57 |
-
echo " Logs: docker-compose logs -f"
|
| 58 |
-
echo " Restart: docker-compose restart"
|
| 59 |
-
echo " Test: python test_production.py"
|
| 60 |
-
echo ""
|
| 61 |
-
echo "⚠️ For production use:"
|
| 62 |
-
echo " 1. Configure HTTPS with SSL certificates"
|
| 63 |
-
echo " 2. Set a secure SECRET_KEY environment variable"
|
| 64 |
-
echo " 3. Configure domain-specific CORS settings"
|
| 65 |
-
echo " 4. Set up monitoring and log aggregation"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/index.html
ADDED
|
@@ -0,0 +1,806 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>PhotoGuard - Image Quality Validation API</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
:root {
|
| 15 |
+
--black: #000000;
|
| 16 |
+
--charcoal: #1a1a1a;
|
| 17 |
+
--dark-gray: #2d2d2d;
|
| 18 |
+
--medium-gray: #6b6b6b;
|
| 19 |
+
--light-gray: #a8a8a8;
|
| 20 |
+
--silver: #cccccc;
|
| 21 |
+
--off-white: #f5f5f5;
|
| 22 |
+
--white: #ffffff;
|
| 23 |
+
--success: #4a4a4a;
|
| 24 |
+
--error: #1a1a1a;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
body {
|
| 28 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 29 |
+
background: var(--off-white);
|
| 30 |
+
color: var(--charcoal);
|
| 31 |
+
line-height: 1.6;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/* Navigation */
|
| 35 |
+
nav {
|
| 36 |
+
background: var(--black);
|
| 37 |
+
border-bottom: 2px solid var(--dark-gray);
|
| 38 |
+
padding: 1rem 0;
|
| 39 |
+
position: sticky;
|
| 40 |
+
top: 0;
|
| 41 |
+
z-index: 100;
|
| 42 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.nav-container {
|
| 46 |
+
max-width: 1200px;
|
| 47 |
+
margin: 0 auto;
|
| 48 |
+
padding: 0 2rem;
|
| 49 |
+
display: flex;
|
| 50 |
+
justify-content: space-between;
|
| 51 |
+
align-items: center;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.logo {
|
| 55 |
+
font-size: 1.5rem;
|
| 56 |
+
font-weight: 700;
|
| 57 |
+
color: var(--white);
|
| 58 |
+
letter-spacing: 0.5px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.nav-links {
|
| 62 |
+
display: flex;
|
| 63 |
+
gap: 2rem;
|
| 64 |
+
list-style: none;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.nav-links a {
|
| 68 |
+
text-decoration: none;
|
| 69 |
+
color: var(--light-gray);
|
| 70 |
+
font-weight: 500;
|
| 71 |
+
transition: color 0.2s;
|
| 72 |
+
text-transform: uppercase;
|
| 73 |
+
font-size: 0.9rem;
|
| 74 |
+
letter-spacing: 1px;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.nav-links a:hover {
|
| 78 |
+
color: var(--white);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* Hero Section */
|
| 82 |
+
.hero {
|
| 83 |
+
background: linear-gradient(135deg, var(--charcoal) 0%, var(--black) 100%);
|
| 84 |
+
color: var(--white);
|
| 85 |
+
padding: 4rem 2rem;
|
| 86 |
+
text-align: center;
|
| 87 |
+
border-bottom: 4px solid var(--dark-gray);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.hero h1 {
|
| 91 |
+
font-size: 3rem;
|
| 92 |
+
margin-bottom: 1rem;
|
| 93 |
+
font-weight: 800;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.hero p {
|
| 97 |
+
font-size: 1.25rem;
|
| 98 |
+
color: var(--light-gray);
|
| 99 |
+
max-width: 700px;
|
| 100 |
+
margin: 0 auto 2rem;
|
| 101 |
+
line-height: 1.8;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/* Container */
|
| 105 |
+
.container {
|
| 106 |
+
max-width: 1200px;
|
| 107 |
+
margin: 0 auto;
|
| 108 |
+
padding: 3rem 2rem;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
section {
|
| 112 |
+
margin-bottom: 4rem;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
section h2 {
|
| 116 |
+
font-size: 2rem;
|
| 117 |
+
margin-bottom: 1rem;
|
| 118 |
+
color: var(--black);
|
| 119 |
+
font-weight: 700;
|
| 120 |
+
letter-spacing: -0.5px;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/* Upload Section */
|
| 124 |
+
.upload-area {
|
| 125 |
+
background: var(--white);
|
| 126 |
+
border: 3px dashed var(--silver);
|
| 127 |
+
border-radius: 4px;
|
| 128 |
+
padding: 3rem;
|
| 129 |
+
text-align: center;
|
| 130 |
+
cursor: pointer;
|
| 131 |
+
transition: all 0.3s;
|
| 132 |
+
margin-bottom: 2rem;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.upload-area:hover {
|
| 136 |
+
border-color: var(--medium-gray);
|
| 137 |
+
background: var(--off-white);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.upload-area.dragover {
|
| 141 |
+
border-color: var(--charcoal);
|
| 142 |
+
background: var(--off-white);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.upload-icon {
|
| 146 |
+
font-size: 4rem;
|
| 147 |
+
margin-bottom: 1rem;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.file-input {
|
| 151 |
+
display: none;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.btn {
|
| 155 |
+
padding: 0.75rem 2rem;
|
| 156 |
+
border-radius: 2px;
|
| 157 |
+
border: none;
|
| 158 |
+
font-weight: 600;
|
| 159 |
+
cursor: pointer;
|
| 160 |
+
transition: all 0.2s;
|
| 161 |
+
font-size: 1rem;
|
| 162 |
+
text-transform: uppercase;
|
| 163 |
+
letter-spacing: 1px;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.btn-primary {
|
| 167 |
+
background: var(--black);
|
| 168 |
+
color: var(--white);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.btn-primary:hover {
|
| 172 |
+
background: var(--charcoal);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.btn-secondary {
|
| 176 |
+
background: var(--white);
|
| 177 |
+
color: var(--black);
|
| 178 |
+
border: 2px solid var(--black);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.btn-secondary:hover {
|
| 182 |
+
background: var(--black);
|
| 183 |
+
color: var(--white);
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/* Results */
|
| 187 |
+
.results {
|
| 188 |
+
display: none;
|
| 189 |
+
background: var(--white);
|
| 190 |
+
border-radius: 4px;
|
| 191 |
+
padding: 2rem;
|
| 192 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
| 193 |
+
border: 1px solid var(--silver);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.results.show {
|
| 197 |
+
display: block;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.status-badge {
|
| 201 |
+
display: inline-block;
|
| 202 |
+
padding: 0.5rem 1rem;
|
| 203 |
+
border-radius: 2px;
|
| 204 |
+
font-weight: 600;
|
| 205 |
+
margin-bottom: 1rem;
|
| 206 |
+
text-transform: uppercase;
|
| 207 |
+
letter-spacing: 1px;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.status-badge.pass {
|
| 211 |
+
background: var(--dark-gray);
|
| 212 |
+
color: var(--white);
|
| 213 |
+
border: 2px solid var(--black);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.status-badge.fail {
|
| 217 |
+
background: var(--white);
|
| 218 |
+
color: var(--black);
|
| 219 |
+
border: 2px solid var(--black);
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.validation-grid {
|
| 223 |
+
display: grid;
|
| 224 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 225 |
+
gap: 1rem;
|
| 226 |
+
margin: 2rem 0;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.validation-card {
|
| 230 |
+
background: var(--off-white);
|
| 231 |
+
border-radius: 2px;
|
| 232 |
+
padding: 1.5rem;
|
| 233 |
+
border-left: 4px solid var(--silver);
|
| 234 |
+
border: 1px solid var(--silver);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.validation-card.pass {
|
| 238 |
+
border-left-color: var(--black);
|
| 239 |
+
background: var(--white);
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.validation-card.fail {
|
| 243 |
+
border-left-color: var(--medium-gray);
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.validation-card h4 {
|
| 247 |
+
margin-bottom: 0.5rem;
|
| 248 |
+
display: flex;
|
| 249 |
+
align-items: center;
|
| 250 |
+
gap: 0.5rem;
|
| 251 |
+
color: var(--charcoal);
|
| 252 |
+
font-weight: 600;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.validation-card .score {
|
| 256 |
+
font-size: 1.5rem;
|
| 257 |
+
font-weight: 700;
|
| 258 |
+
color: var(--black);
|
| 259 |
+
margin: 0.5rem 0;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
/* How It Works */
|
| 263 |
+
.steps {
|
| 264 |
+
display: grid;
|
| 265 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 266 |
+
gap: 2rem;
|
| 267 |
+
margin-top: 2rem;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.step {
|
| 271 |
+
text-align: center;
|
| 272 |
+
padding: 2rem;
|
| 273 |
+
background: var(--white);
|
| 274 |
+
border-radius: 2px;
|
| 275 |
+
border: 1px solid var(--silver);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.step-number {
|
| 279 |
+
width: 60px;
|
| 280 |
+
height: 60px;
|
| 281 |
+
background: var(--black);
|
| 282 |
+
color: var(--white);
|
| 283 |
+
border-radius: 50%;
|
| 284 |
+
display: flex;
|
| 285 |
+
align-items: center;
|
| 286 |
+
justify-content: center;
|
| 287 |
+
font-size: 1.5rem;
|
| 288 |
+
font-weight: 700;
|
| 289 |
+
margin: 0 auto 1rem;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.step h3 {
|
| 293 |
+
margin-bottom: 0.5rem;
|
| 294 |
+
color: var(--charcoal);
|
| 295 |
+
font-weight: 600;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.step p {
|
| 299 |
+
color: var(--medium-gray);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
/* API Documentation */
|
| 303 |
+
.api-card {
|
| 304 |
+
background: var(--white);
|
| 305 |
+
border-radius: 2px;
|
| 306 |
+
padding: 2rem;
|
| 307 |
+
margin-bottom: 1.5rem;
|
| 308 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 309 |
+
border: 1px solid var(--silver);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.api-card h3 {
|
| 313 |
+
margin-bottom: 1rem;
|
| 314 |
+
color: var(--black);
|
| 315 |
+
font-weight: 700;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.api-buttons {
|
| 319 |
+
display: flex;
|
| 320 |
+
gap: 1rem;
|
| 321 |
+
margin-top: 2rem;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.api-btn {
|
| 325 |
+
flex: 1;
|
| 326 |
+
padding: 2rem;
|
| 327 |
+
background: var(--white);
|
| 328 |
+
border: 2px solid var(--black);
|
| 329 |
+
border-radius: 2px;
|
| 330 |
+
text-align: center;
|
| 331 |
+
cursor: pointer;
|
| 332 |
+
transition: all 0.3s;
|
| 333 |
+
text-decoration: none;
|
| 334 |
+
color: var(--charcoal);
|
| 335 |
+
display: block;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.api-btn:hover {
|
| 339 |
+
background: var(--black);
|
| 340 |
+
color: var(--white);
|
| 341 |
+
transform: translateY(-2px);
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.api-btn:hover h4 {
|
| 345 |
+
color: var(--white);
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.api-btn:hover p {
|
| 349 |
+
color: var(--light-gray);
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.api-btn h4 {
|
| 353 |
+
font-size: 1.25rem;
|
| 354 |
+
margin-bottom: 0.5rem;
|
| 355 |
+
color: var(--black);
|
| 356 |
+
font-weight: 700;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.api-btn p {
|
| 360 |
+
color: var(--medium-gray);
|
| 361 |
+
font-size: 0.9rem;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.endpoint {
|
| 365 |
+
background: var(--off-white);
|
| 366 |
+
border-left: 4px solid var(--black);
|
| 367 |
+
padding: 1rem;
|
| 368 |
+
margin: 1rem 0;
|
| 369 |
+
border-radius: 2px;
|
| 370 |
+
border: 1px solid var(--silver);
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.endpoint-method {
|
| 374 |
+
display: inline-block;
|
| 375 |
+
background: var(--black);
|
| 376 |
+
color: var(--white);
|
| 377 |
+
padding: 0.25rem 0.75rem;
|
| 378 |
+
border-radius: 2px;
|
| 379 |
+
font-weight: 600;
|
| 380 |
+
font-size: 0.8rem;
|
| 381 |
+
margin-right: 0.5rem;
|
| 382 |
+
text-transform: uppercase;
|
| 383 |
+
letter-spacing: 1px;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.endpoint-method.get {
|
| 387 |
+
background: var(--dark-gray);
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.endpoint-path {
|
| 391 |
+
font-family: 'Courier New', monospace;
|
| 392 |
+
color: var(--charcoal);
|
| 393 |
+
font-weight: 600;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.endpoint-desc {
|
| 397 |
+
margin-top: 0.5rem;
|
| 398 |
+
color: var(--medium-gray);
|
| 399 |
+
font-size: 0.9rem;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.features-grid {
|
| 403 |
+
display: grid;
|
| 404 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 405 |
+
gap: 1.5rem;
|
| 406 |
+
margin-top: 2rem;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.feature-item {
|
| 410 |
+
display: flex;
|
| 411 |
+
align-items: flex-start;
|
| 412 |
+
gap: 0.75rem;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.feature-icon {
|
| 416 |
+
font-size: 1.5rem;
|
| 417 |
+
flex-shrink: 0;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
.loading {
|
| 421 |
+
display: none;
|
| 422 |
+
text-align: center;
|
| 423 |
+
padding: 2rem;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.loading.show {
|
| 427 |
+
display: block;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.spinner {
|
| 431 |
+
border: 4px solid var(--silver);
|
| 432 |
+
border-top: 4px solid var(--black);
|
| 433 |
+
border-radius: 50%;
|
| 434 |
+
width: 50px;
|
| 435 |
+
height: 50px;
|
| 436 |
+
animation: spin 1s linear infinite;
|
| 437 |
+
margin: 0 auto;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
@keyframes spin {
|
| 441 |
+
0% { transform: rotate(0deg); }
|
| 442 |
+
100% { transform: rotate(360deg); }
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
/* Responsive */
|
| 446 |
+
@media (max-width: 768px) {
|
| 447 |
+
.hero h1 {
|
| 448 |
+
font-size: 2rem;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
.nav-links {
|
| 452 |
+
gap: 1rem;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
.api-buttons {
|
| 456 |
+
flex-direction: column;
|
| 457 |
+
}
|
| 458 |
+
}
|
| 459 |
+
</style>
|
| 460 |
+
</head>
|
| 461 |
+
<body>
|
| 462 |
+
<!-- Navigation -->
|
| 463 |
+
<nav>
|
| 464 |
+
<div class="nav-container">
|
| 465 |
+
<div class="logo">�️ PhotoGuard</div>
|
| 466 |
+
<ul class="nav-links">
|
| 467 |
+
<li><a href="#home">Home</a></li>
|
| 468 |
+
<li><a href="#how-it-works">How It Works</a></li>
|
| 469 |
+
<li><a href="#api-docs">API Docs</a></li>
|
| 470 |
+
</ul>
|
| 471 |
+
</div>
|
| 472 |
+
</nav>
|
| 473 |
+
|
| 474 |
+
<!-- Hero Section -->
|
| 475 |
+
<div class="hero" id="home">
|
| 476 |
+
<h1>PhotoGuard</h1>
|
| 477 |
+
<p>Professional image quality validation API with automated blur detection, brightness analysis, resolution checking, exposure verification, and metadata extraction</p>
|
| 478 |
+
</div>
|
| 479 |
+
|
| 480 |
+
<!-- Main Container -->
|
| 481 |
+
<div class="container">
|
| 482 |
+
<!-- Upload Section -->
|
| 483 |
+
<section>
|
| 484 |
+
<h2>Upload Image for Validation</h2>
|
| 485 |
+
<div class="upload-area" id="uploadArea">
|
| 486 |
+
<div class="upload-icon">📁</div>
|
| 487 |
+
<h3>Drop your image here or click to browse</h3>
|
| 488 |
+
<p style="color: var(--medium-gray); margin-top: 0.5rem;">Supports JPG, PNG, HEIC (Max 16MB)</p>
|
| 489 |
+
<input type="file" id="fileInput" class="file-input" accept="image/*">
|
| 490 |
+
</div>
|
| 491 |
+
|
| 492 |
+
<!-- Loading State -->
|
| 493 |
+
<div class="loading" id="loading">
|
| 494 |
+
<div class="spinner"></div>
|
| 495 |
+
<p style="margin-top: 1rem; color: var(--medium-gray);">Analyzing image quality...</p>
|
| 496 |
+
</div>
|
| 497 |
+
|
| 498 |
+
<!-- Results -->
|
| 499 |
+
<div class="results" id="results">
|
| 500 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
| 501 |
+
<h3>Validation Results</h3>
|
| 502 |
+
<button class="btn btn-secondary" onclick="resetUpload()">Upload Another Image</button>
|
| 503 |
+
</div>
|
| 504 |
+
|
| 505 |
+
<div id="statusBadge"></div>
|
| 506 |
+
<div id="validationGrid" class="validation-grid"></div>
|
| 507 |
+
<div id="recommendations"></div>
|
| 508 |
+
</div>
|
| 509 |
+
</section>
|
| 510 |
+
|
| 511 |
+
<!-- How It Works -->
|
| 512 |
+
<section id="how-it-works">
|
| 513 |
+
<h2>How It Works</h2>
|
| 514 |
+
<div class="steps">
|
| 515 |
+
<div class="step">
|
| 516 |
+
<div class="step-number">1</div>
|
| 517 |
+
<h3>Upload Image</h3>
|
| 518 |
+
<p>Drag & drop or browse to select an image file from your device</p>
|
| 519 |
+
</div>
|
| 520 |
+
<div class="step">
|
| 521 |
+
<div class="step-number">2</div>
|
| 522 |
+
<h3>Quality Analysis</h3>
|
| 523 |
+
<p>Automated 5-layer validation checks every aspect of image quality</p>
|
| 524 |
+
</div>
|
| 525 |
+
<div class="step">
|
| 526 |
+
<div class="step-number">3</div>
|
| 527 |
+
<h3>Receive Report</h3>
|
| 528 |
+
<p>Get instant validation results with scores and improvement recommendations</p>
|
| 529 |
+
</div>
|
| 530 |
+
</div>
|
| 531 |
+
</section>
|
| 532 |
+
|
| 533 |
+
<!-- API Documentation -->
|
| 534 |
+
<section id="api-docs">
|
| 535 |
+
<h2>API Documentation</h2>
|
| 536 |
+
|
| 537 |
+
<div class="api-card">
|
| 538 |
+
<h3>Interactive API Documentation</h3>
|
| 539 |
+
<p>Test and explore PhotoGuard API endpoints directly in your browser with interactive documentation tools</p>
|
| 540 |
+
</div>
|
| 541 |
+
|
| 542 |
+
<div class="api-buttons">
|
| 543 |
+
<div class="api-btn" onclick="window.open('/api/docs', '_blank')">
|
| 544 |
+
<h4>📘 Swagger UI</h4>
|
| 545 |
+
<p>Interactive testing interface with live API execution and request builder</p>
|
| 546 |
+
</div>
|
| 547 |
+
<div class="api-btn" onclick="window.open('/api/redoc', '_blank')">
|
| 548 |
+
<h4>📖 ReDoc</h4>
|
| 549 |
+
<p>Comprehensive API reference with detailed schemas and examples</p>
|
| 550 |
+
</div>
|
| 551 |
+
</div>
|
| 552 |
+
|
| 553 |
+
<div class="api-card" style="margin-top: 2rem;">
|
| 554 |
+
<h3>Available Endpoints</h3>
|
| 555 |
+
|
| 556 |
+
<div class="endpoint">
|
| 557 |
+
<span class="endpoint-method">POST</span>
|
| 558 |
+
<span class="endpoint-path">/api/validate</span>
|
| 559 |
+
<div class="endpoint-desc">
|
| 560 |
+
Upload and validate image quality across all parameters (blur, brightness, resolution, exposure, metadata)
|
| 561 |
+
<br><strong>Request:</strong> multipart/form-data with image file
|
| 562 |
+
<br><strong>Response:</strong> Complete validation report with scores and recommendations
|
| 563 |
+
</div>
|
| 564 |
+
</div>
|
| 565 |
+
|
| 566 |
+
<div class="endpoint">
|
| 567 |
+
<span class="endpoint-method get">GET</span>
|
| 568 |
+
<span class="endpoint-path">/api/validation-rules</span>
|
| 569 |
+
<div class="endpoint-desc">
|
| 570 |
+
Retrieve current validation thresholds and quality requirements
|
| 571 |
+
<br><strong>Response:</strong> Configuration rules with threshold values
|
| 572 |
+
</div>
|
| 573 |
+
</div>
|
| 574 |
+
|
| 575 |
+
<div class="endpoint">
|
| 576 |
+
<span class="endpoint-method get">GET</span>
|
| 577 |
+
<span class="endpoint-path">/api/summary</span>
|
| 578 |
+
<div class="endpoint-desc">
|
| 579 |
+
View processing statistics and quality metrics
|
| 580 |
+
<br><strong>Response:</strong> Aggregated data on processed images and acceptance rates
|
| 581 |
+
</div>
|
| 582 |
+
</div>
|
| 583 |
+
|
| 584 |
+
<div class="endpoint">
|
| 585 |
+
<span class="endpoint-method get">GET</span>
|
| 586 |
+
<span class="endpoint-path">/api/health</span>
|
| 587 |
+
<div class="endpoint-desc">
|
| 588 |
+
Check API service health and availability status
|
| 589 |
+
<br><strong>Response:</strong> Service status, API version, and system information
|
| 590 |
+
</div>
|
| 591 |
+
</div>
|
| 592 |
+
</div>
|
| 593 |
+
|
| 594 |
+
<div class="api-card">
|
| 595 |
+
<h3>Core Validation Features</h3>
|
| 596 |
+
<div class="features-grid">
|
| 597 |
+
<div class="feature-item">
|
| 598 |
+
<span class="feature-icon">▪</span>
|
| 599 |
+
<div>
|
| 600 |
+
<strong>Blur Detection</strong><br>
|
| 601 |
+
<span style="color: var(--medium-gray); font-size: 0.9rem;">Advanced Laplacian variance analysis measures image sharpness</span>
|
| 602 |
+
</div>
|
| 603 |
+
</div>
|
| 604 |
+
<div class="feature-item">
|
| 605 |
+
<span class="feature-icon">▪</span>
|
| 606 |
+
<div>
|
| 607 |
+
<strong>Brightness Validation</strong><br>
|
| 608 |
+
<span style="color: var(--medium-gray); font-size: 0.9rem;">Pixel intensity analysis with optimal 50-220 luminance range</span>
|
| 609 |
+
</div>
|
| 610 |
+
</div>
|
| 611 |
+
<div class="feature-item">
|
| 612 |
+
<span class="feature-icon">▪</span>
|
| 613 |
+
<div>
|
| 614 |
+
<strong>Resolution Verification</strong><br>
|
| 615 |
+
<span style="color: var(--medium-gray); font-size: 0.9rem;">Validates minimum 800×600 pixels and 0.5MP requirement</span>
|
| 616 |
+
</div>
|
| 617 |
+
</div>
|
| 618 |
+
<div class="feature-item">
|
| 619 |
+
<span class="feature-icon">▪</span>
|
| 620 |
+
<div>
|
| 621 |
+
<strong>Exposure Analysis</strong><br>
|
| 622 |
+
<span style="color: var(--medium-gray); font-size: 0.9rem;">Dynamic range assessment with clipping detection</span>
|
| 623 |
+
</div>
|
| 624 |
+
</div>
|
| 625 |
+
<div class="feature-item">
|
| 626 |
+
<span class="feature-icon">▪</span>
|
| 627 |
+
<div>
|
| 628 |
+
<strong>Metadata Extraction</strong><br>
|
| 629 |
+
<span style="color: var(--medium-gray); font-size: 0.9rem;">Complete EXIF data parsing including GPS coordinates</span>
|
| 630 |
+
</div>
|
| 631 |
+
</div>
|
| 632 |
+
<div class="feature-item">
|
| 633 |
+
<span class="feature-icon">▪</span>
|
| 634 |
+
<div>
|
| 635 |
+
<strong>Intelligent Scoring</strong><br>
|
| 636 |
+
<span style="color: var(--medium-gray); font-size: 0.9rem;">Weighted algorithm with 65% pass threshold and partial credit</span>
|
| 637 |
+
</div>
|
| 638 |
+
</div>
|
| 639 |
+
</div>
|
| 640 |
+
</div>
|
| 641 |
+
</section>
|
| 642 |
+
</div>
|
| 643 |
+
|
| 644 |
+
<script>
|
| 645 |
+
const uploadArea = document.getElementById('uploadArea');
|
| 646 |
+
const fileInput = document.getElementById('fileInput');
|
| 647 |
+
const loading = document.getElementById('loading');
|
| 648 |
+
const results = document.getElementById('results');
|
| 649 |
+
|
| 650 |
+
// Upload area click
|
| 651 |
+
uploadArea.addEventListener('click', () => fileInput.click());
|
| 652 |
+
|
| 653 |
+
// Drag and drop
|
| 654 |
+
uploadArea.addEventListener('dragover', (e) => {
|
| 655 |
+
e.preventDefault();
|
| 656 |
+
uploadArea.classList.add('dragover');
|
| 657 |
+
});
|
| 658 |
+
|
| 659 |
+
uploadArea.addEventListener('dragleave', () => {
|
| 660 |
+
uploadArea.classList.remove('dragover');
|
| 661 |
+
});
|
| 662 |
+
|
| 663 |
+
uploadArea.addEventListener('drop', (e) => {
|
| 664 |
+
e.preventDefault();
|
| 665 |
+
uploadArea.classList.remove('dragover');
|
| 666 |
+
const file = e.dataTransfer.files[0];
|
| 667 |
+
if (file && file.type.startsWith('image/')) {
|
| 668 |
+
handleFileUpload(file);
|
| 669 |
+
}
|
| 670 |
+
});
|
| 671 |
+
|
| 672 |
+
// File input change
|
| 673 |
+
fileInput.addEventListener('change', (e) => {
|
| 674 |
+
const file = e.target.files[0];
|
| 675 |
+
if (file) {
|
| 676 |
+
handleFileUpload(file);
|
| 677 |
+
}
|
| 678 |
+
});
|
| 679 |
+
|
| 680 |
+
async function handleFileUpload(file) {
|
| 681 |
+
uploadArea.style.display = 'none';
|
| 682 |
+
loading.classList.add('show');
|
| 683 |
+
results.classList.remove('show');
|
| 684 |
+
|
| 685 |
+
const formData = new FormData();
|
| 686 |
+
formData.append('image', file);
|
| 687 |
+
|
| 688 |
+
try {
|
| 689 |
+
const response = await fetch('/api/validate', {
|
| 690 |
+
method: 'POST',
|
| 691 |
+
body: formData
|
| 692 |
+
});
|
| 693 |
+
|
| 694 |
+
const data = await response.json();
|
| 695 |
+
|
| 696 |
+
loading.classList.remove('show');
|
| 697 |
+
displayResults(data);
|
| 698 |
+
} catch (error) {
|
| 699 |
+
loading.classList.remove('show');
|
| 700 |
+
alert('Error uploading image: ' + error.message);
|
| 701 |
+
uploadArea.style.display = 'block';
|
| 702 |
+
}
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
function displayResults(data) {
|
| 706 |
+
if (!data.success) {
|
| 707 |
+
alert('Validation failed: ' + data.message);
|
| 708 |
+
uploadArea.style.display = 'block';
|
| 709 |
+
return;
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
const summary = data.data.summary;
|
| 713 |
+
const checks = data.data.checks;
|
| 714 |
+
|
| 715 |
+
// Status badge
|
| 716 |
+
const statusBadge = document.getElementById('statusBadge');
|
| 717 |
+
const isPassing = summary.overall_status === 'pass';
|
| 718 |
+
statusBadge.innerHTML = `
|
| 719 |
+
<div class="status-badge ${isPassing ? 'pass' : 'fail'}">
|
| 720 |
+
${isPassing ? '✓' : '✕'} ${summary.overall_status.toUpperCase()}
|
| 721 |
+
(Score: ${summary.overall_score.toFixed(1)}%)
|
| 722 |
+
</div>
|
| 723 |
+
`;
|
| 724 |
+
|
| 725 |
+
// Validation cards
|
| 726 |
+
const grid = document.getElementById('validationGrid');
|
| 727 |
+
grid.innerHTML = '';
|
| 728 |
+
|
| 729 |
+
const checkLabels = {
|
| 730 |
+
blur: '🔍 Blur Detection',
|
| 731 |
+
brightness: '💡 Brightness',
|
| 732 |
+
resolution: '📏 Resolution',
|
| 733 |
+
exposure: '☀️ Exposure',
|
| 734 |
+
metadata: '📋 Metadata'
|
| 735 |
+
};
|
| 736 |
+
|
| 737 |
+
for (const [key, check] of Object.entries(checks)) {
|
| 738 |
+
const isPassing = check.status === 'pass';
|
| 739 |
+
const score = getScore(key, check);
|
| 740 |
+
const unit = key === 'resolution' ? ' MP' : '%';
|
| 741 |
+
|
| 742 |
+
const card = document.createElement('div');
|
| 743 |
+
card.className = `validation-card ${isPassing ? 'pass' : 'fail'}`;
|
| 744 |
+
card.innerHTML = `
|
| 745 |
+
<h4>
|
| 746 |
+
${checkLabels[key] || key}
|
| 747 |
+
<span style="margin-left: auto;">${isPassing ? '✓' : '✕'}</span>
|
| 748 |
+
</h4>
|
| 749 |
+
<div class="score">${score}${unit}</div>
|
| 750 |
+
<p style="color: var(--gray-600); font-size: 0.9rem;">${check.reason || ''}</p>
|
| 751 |
+
`;
|
| 752 |
+
grid.appendChild(card);
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
// Recommendations
|
| 756 |
+
const recommendations = document.getElementById('recommendations');
|
| 757 |
+
if (summary.recommendations && summary.recommendations.length > 0) {
|
| 758 |
+
recommendations.innerHTML = `
|
| 759 |
+
<h4 style="margin-top: 2rem; margin-bottom: 1rem;">Recommendations</h4>
|
| 760 |
+
<ul style="padding-left: 1.5rem; color: var(--gray-600);">
|
| 761 |
+
${summary.recommendations.map(r => `<li>${r}</li>`).join('')}
|
| 762 |
+
</ul>
|
| 763 |
+
`;
|
| 764 |
+
} else {
|
| 765 |
+
recommendations.innerHTML = '';
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
results.classList.add('show');
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
function getScore(checkType, check) {
|
| 772 |
+
switch(checkType) {
|
| 773 |
+
case 'blur':
|
| 774 |
+
return check.score?.toFixed(0) || '--';
|
| 775 |
+
case 'brightness':
|
| 776 |
+
return check.mean_brightness?.toFixed(0) || '--';
|
| 777 |
+
case 'resolution':
|
| 778 |
+
return check.megapixels?.toFixed(1) || '--';
|
| 779 |
+
case 'exposure':
|
| 780 |
+
return check.dynamic_range?.toFixed(0) || '--';
|
| 781 |
+
case 'metadata':
|
| 782 |
+
return check.completeness?.toFixed(0) || '--';
|
| 783 |
+
default:
|
| 784 |
+
return check.score?.toFixed(0) || '--';
|
| 785 |
+
}
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
function resetUpload() {
|
| 789 |
+
results.classList.remove('show');
|
| 790 |
+
uploadArea.style.display = 'block';
|
| 791 |
+
fileInput.value = '';
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
// Smooth scroll for navigation
|
| 795 |
+
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
| 796 |
+
anchor.addEventListener('click', function (e) {
|
| 797 |
+
e.preventDefault();
|
| 798 |
+
const target = document.querySelector(this.getAttribute('href'));
|
| 799 |
+
if (target) {
|
| 800 |
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 801 |
+
}
|
| 802 |
+
});
|
| 803 |
+
});
|
| 804 |
+
</script>
|
| 805 |
+
</body>
|
| 806 |
+
</html>
|
templates/mobile_upload.html
DELETED
|
@@ -1,624 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>Civic Quality Control - Photo Upload</title>
|
| 7 |
-
<style>
|
| 8 |
-
* {
|
| 9 |
-
margin: 0;
|
| 10 |
-
padding: 0;
|
| 11 |
-
box-sizing: border-box;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
body {
|
| 15 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 16 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 17 |
-
min-height: 100vh;
|
| 18 |
-
display: flex;
|
| 19 |
-
align-items: center;
|
| 20 |
-
justify-content: center;
|
| 21 |
-
padding: 10px;
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
.container {
|
| 25 |
-
background: white;
|
| 26 |
-
border-radius: 20px;
|
| 27 |
-
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
| 28 |
-
padding: 30px;
|
| 29 |
-
max-width: 500px;
|
| 30 |
-
width: 100%;
|
| 31 |
-
text-align: center;
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
.header {
|
| 35 |
-
margin-bottom: 30px;
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
.header h1 {
|
| 39 |
-
color: #333;
|
| 40 |
-
font-size: 28px;
|
| 41 |
-
margin-bottom: 10px;
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
.header p {
|
| 45 |
-
color: #666;
|
| 46 |
-
font-size: 16px;
|
| 47 |
-
line-height: 1.5;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
.upload-section {
|
| 51 |
-
margin-bottom: 30px;
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
.camera-preview {
|
| 55 |
-
display: none;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
#video {
|
| 59 |
-
width: 100%;
|
| 60 |
-
max-width: 400px;
|
| 61 |
-
border-radius: 15px;
|
| 62 |
-
margin-bottom: 20px;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
#canvas {
|
| 66 |
-
display: none;
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
.captured-image {
|
| 70 |
-
max-width: 100%;
|
| 71 |
-
border-radius: 15px;
|
| 72 |
-
margin-bottom: 20px;
|
| 73 |
-
display: none;
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
.file-input-wrapper {
|
| 77 |
-
position: relative;
|
| 78 |
-
overflow: hidden;
|
| 79 |
-
display: inline-block;
|
| 80 |
-
width: 100%;
|
| 81 |
-
margin-bottom: 20px;
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
.file-input {
|
| 85 |
-
position: absolute;
|
| 86 |
-
left: -9999px;
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
.btn {
|
| 90 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 91 |
-
color: white;
|
| 92 |
-
border: none;
|
| 93 |
-
padding: 15px 30px;
|
| 94 |
-
border-radius: 50px;
|
| 95 |
-
font-size: 16px;
|
| 96 |
-
font-weight: 600;
|
| 97 |
-
cursor: pointer;
|
| 98 |
-
transition: all 0.3s ease;
|
| 99 |
-
width: 100%;
|
| 100 |
-
margin-bottom: 15px;
|
| 101 |
-
display: flex;
|
| 102 |
-
align-items: center;
|
| 103 |
-
justify-content: center;
|
| 104 |
-
gap: 10px;
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
.btn:hover {
|
| 108 |
-
transform: translateY(-2px);
|
| 109 |
-
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
.btn:active {
|
| 113 |
-
transform: translateY(0);
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
.btn.secondary {
|
| 117 |
-
background: #f8f9fa;
|
| 118 |
-
color: #333;
|
| 119 |
-
border: 2px solid #e9ecef;
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
.btn.danger {
|
| 123 |
-
background: #dc3545;
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
.btn:disabled {
|
| 127 |
-
opacity: 0.6;
|
| 128 |
-
cursor: not-allowed;
|
| 129 |
-
transform: none;
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
.loading {
|
| 133 |
-
display: none;
|
| 134 |
-
align-items: center;
|
| 135 |
-
justify-content: center;
|
| 136 |
-
gap: 10px;
|
| 137 |
-
margin: 20px 0;
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
.spinner {
|
| 141 |
-
width: 20px;
|
| 142 |
-
height: 20px;
|
| 143 |
-
border: 2px solid #f3f3f3;
|
| 144 |
-
border-top: 2px solid #667eea;
|
| 145 |
-
border-radius: 50%;
|
| 146 |
-
animation: spin 1s linear infinite;
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
@keyframes spin {
|
| 150 |
-
0% { transform: rotate(0deg); }
|
| 151 |
-
100% { transform: rotate(360deg); }
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
.results {
|
| 155 |
-
display: none;
|
| 156 |
-
text-align: left;
|
| 157 |
-
background: #f8f9fa;
|
| 158 |
-
padding: 20px;
|
| 159 |
-
border-radius: 15px;
|
| 160 |
-
margin-top: 20px;
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
.status-badge {
|
| 164 |
-
display: inline-block;
|
| 165 |
-
padding: 8px 16px;
|
| 166 |
-
border-radius: 20px;
|
| 167 |
-
font-size: 14px;
|
| 168 |
-
font-weight: 600;
|
| 169 |
-
margin-bottom: 15px;
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
.status-excellent { background: #d4edda; color: #155724; }
|
| 173 |
-
.status-good { background: #d1ecf1; color: #0c5460; }
|
| 174 |
-
.status-acceptable { background: #fff3cd; color: #856404; }
|
| 175 |
-
.status-rejected { background: #f8d7da; color: #721c24; }
|
| 176 |
-
.status-error { background: #f8d7da; color: #721c24; }
|
| 177 |
-
|
| 178 |
-
.validation-item {
|
| 179 |
-
margin-bottom: 15px;
|
| 180 |
-
padding: 12px;
|
| 181 |
-
background: white;
|
| 182 |
-
border-radius: 8px;
|
| 183 |
-
border-left: 4px solid #667eea;
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
.validation-item h4 {
|
| 187 |
-
margin-bottom: 5px;
|
| 188 |
-
color: #333;
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
.validation-item.error {
|
| 192 |
-
border-left-color: #dc3545;
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
.validation-item.warning {
|
| 196 |
-
border-left-color: #ffc107;
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
.validation-item.success {
|
| 200 |
-
border-left-color: #28a745;
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
.issues-list {
|
| 204 |
-
list-style: none;
|
| 205 |
-
margin: 10px 0;
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
.issues-list li {
|
| 209 |
-
padding: 5px 0;
|
| 210 |
-
border-bottom: 1px solid #eee;
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
.issues-list li:last-child {
|
| 214 |
-
border-bottom: none;
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
.metric {
|
| 218 |
-
display: flex;
|
| 219 |
-
justify-content: space-between;
|
| 220 |
-
margin-bottom: 8px;
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
.metric strong {
|
| 224 |
-
color: #333;
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
@media (max-width: 480px) {
|
| 228 |
-
.container {
|
| 229 |
-
padding: 20px;
|
| 230 |
-
margin: 10px;
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
.header h1 {
|
| 234 |
-
font-size: 24px;
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
.btn {
|
| 238 |
-
padding: 12px 20px;
|
| 239 |
-
font-size: 14px;
|
| 240 |
-
}
|
| 241 |
-
}
|
| 242 |
-
</style>
|
| 243 |
-
</head>
|
| 244 |
-
<body>
|
| 245 |
-
<div class="container">
|
| 246 |
-
<div class="header">
|
| 247 |
-
<h1>📸 Civic Quality Control</h1>
|
| 248 |
-
<p>Take or upload a photo to automatically check image quality for civic reporting</p>
|
| 249 |
-
</div>
|
| 250 |
-
|
| 251 |
-
<div class="upload-section">
|
| 252 |
-
<!-- Camera Preview -->
|
| 253 |
-
<div class="camera-preview" id="cameraPreview">
|
| 254 |
-
<video id="video" autoplay muted playsinline></video>
|
| 255 |
-
<canvas id="canvas"></canvas>
|
| 256 |
-
<img id="capturedImage" class="captured-image" alt="Captured photo">
|
| 257 |
-
</div>
|
| 258 |
-
|
| 259 |
-
<!-- File Input -->
|
| 260 |
-
<div class="file-input-wrapper" id="fileInputWrapper">
|
| 261 |
-
<input type="file" id="fileInput" class="file-input" accept="image/*" capture="environment">
|
| 262 |
-
<button class="btn" onclick="document.getElementById('fileInput').click()">
|
| 263 |
-
📷 Take Photo or Upload Image
|
| 264 |
-
</button>
|
| 265 |
-
</div>
|
| 266 |
-
|
| 267 |
-
<!-- Action Buttons -->
|
| 268 |
-
<div id="actionButtons" style="display: none;">
|
| 269 |
-
<button class="btn" id="captureBtn" onclick="capturePhoto()">
|
| 270 |
-
📸 Capture Photo
|
| 271 |
-
</button>
|
| 272 |
-
<button class="btn secondary" id="retakeBtn" onclick="retakePhoto()" style="display: none;">
|
| 273 |
-
🔄 Retake Photo
|
| 274 |
-
</button>
|
| 275 |
-
<button class="btn secondary" id="stopCameraBtn" onclick="stopCamera()">
|
| 276 |
-
❌ Stop Camera
|
| 277 |
-
</button>
|
| 278 |
-
</div>
|
| 279 |
-
|
| 280 |
-
<!-- Process Button -->
|
| 281 |
-
<button class="btn" id="processBtn" onclick="processImage()" style="display: none;">
|
| 282 |
-
🔍 Analyze Photo Quality
|
| 283 |
-
</button>
|
| 284 |
-
|
| 285 |
-
<!-- Loading -->
|
| 286 |
-
<div class="loading" id="loading">
|
| 287 |
-
<div class="spinner"></div>
|
| 288 |
-
<span>Analyzing photo quality...</span>
|
| 289 |
-
</div>
|
| 290 |
-
</div>
|
| 291 |
-
|
| 292 |
-
<!-- Results -->
|
| 293 |
-
<div class="results" id="results">
|
| 294 |
-
<div id="resultsContent"></div>
|
| 295 |
-
<button class="btn secondary" onclick="resetApp()">
|
| 296 |
-
📷 Upload Another Photo
|
| 297 |
-
</button>
|
| 298 |
-
</div>
|
| 299 |
-
</div>
|
| 300 |
-
|
| 301 |
-
<script>
|
| 302 |
-
let stream = null;
|
| 303 |
-
let capturedImageData = null;
|
| 304 |
-
|
| 305 |
-
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
|
| 306 |
-
|
| 307 |
-
function handleFileSelect(event) {
|
| 308 |
-
const file = event.target.files[0];
|
| 309 |
-
if (file) {
|
| 310 |
-
const reader = new FileReader();
|
| 311 |
-
reader.onload = function(e) {
|
| 312 |
-
const img = document.getElementById('capturedImage');
|
| 313 |
-
img.src = e.target.result;
|
| 314 |
-
img.style.display = 'block';
|
| 315 |
-
|
| 316 |
-
// Store the file for processing
|
| 317 |
-
capturedImageData = file;
|
| 318 |
-
|
| 319 |
-
// Show process button
|
| 320 |
-
document.getElementById('processBtn').style.display = 'block';
|
| 321 |
-
document.getElementById('fileInputWrapper').style.display = 'none';
|
| 322 |
-
};
|
| 323 |
-
reader.readAsDataURL(file);
|
| 324 |
-
}
|
| 325 |
-
}
|
| 326 |
-
|
| 327 |
-
async function startCamera() {
|
| 328 |
-
try {
|
| 329 |
-
stream = await navigator.mediaDevices.getUserMedia({
|
| 330 |
-
video: {
|
| 331 |
-
facingMode: 'environment', // Use back camera on mobile
|
| 332 |
-
width: { ideal: 1920 },
|
| 333 |
-
height: { ideal: 1080 }
|
| 334 |
-
}
|
| 335 |
-
});
|
| 336 |
-
|
| 337 |
-
const video = document.getElementById('video');
|
| 338 |
-
video.srcObject = stream;
|
| 339 |
-
|
| 340 |
-
document.getElementById('cameraPreview').style.display = 'block';
|
| 341 |
-
document.getElementById('fileInputWrapper').style.display = 'none';
|
| 342 |
-
document.getElementById('actionButtons').style.display = 'block';
|
| 343 |
-
|
| 344 |
-
} catch (err) {
|
| 345 |
-
console.error('Error accessing camera:', err);
|
| 346 |
-
alert('Camera access failed. Please use file upload instead.');
|
| 347 |
-
}
|
| 348 |
-
}
|
| 349 |
-
|
| 350 |
-
function capturePhoto() {
|
| 351 |
-
const video = document.getElementById('video');
|
| 352 |
-
const canvas = document.getElementById('canvas');
|
| 353 |
-
const capturedImage = document.getElementById('capturedImage');
|
| 354 |
-
|
| 355 |
-
canvas.width = video.videoWidth;
|
| 356 |
-
canvas.height = video.videoHeight;
|
| 357 |
-
|
| 358 |
-
const ctx = canvas.getContext('2d');
|
| 359 |
-
ctx.drawImage(video, 0, 0);
|
| 360 |
-
|
| 361 |
-
// Convert to blob
|
| 362 |
-
canvas.toBlob(function(blob) {
|
| 363 |
-
capturedImageData = new File([blob], 'captured_photo.jpg', { type: 'image/jpeg' });
|
| 364 |
-
|
| 365 |
-
const imageUrl = URL.createObjectURL(blob);
|
| 366 |
-
capturedImage.src = imageUrl;
|
| 367 |
-
capturedImage.style.display = 'block';
|
| 368 |
-
}, 'image/jpeg', 0.9);
|
| 369 |
-
|
| 370 |
-
// Hide video, show captured image
|
| 371 |
-
video.style.display = 'none';
|
| 372 |
-
document.getElementById('captureBtn').style.display = 'none';
|
| 373 |
-
document.getElementById('retakeBtn').style.display = 'block';
|
| 374 |
-
document.getElementById('processBtn').style.display = 'block';
|
| 375 |
-
}
|
| 376 |
-
|
| 377 |
-
function retakePhoto() {
|
| 378 |
-
const video = document.getElementById('video');
|
| 379 |
-
const capturedImage = document.getElementById('capturedImage');
|
| 380 |
-
|
| 381 |
-
video.style.display = 'block';
|
| 382 |
-
capturedImage.style.display = 'none';
|
| 383 |
-
|
| 384 |
-
document.getElementById('captureBtn').style.display = 'block';
|
| 385 |
-
document.getElementById('retakeBtn').style.display = 'none';
|
| 386 |
-
document.getElementById('processBtn').style.display = 'none';
|
| 387 |
-
|
| 388 |
-
capturedImageData = null;
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
function stopCamera() {
|
| 392 |
-
if (stream) {
|
| 393 |
-
stream.getTracks().forEach(track => track.stop());
|
| 394 |
-
stream = null;
|
| 395 |
-
}
|
| 396 |
-
|
| 397 |
-
document.getElementById('cameraPreview').style.display = 'none';
|
| 398 |
-
document.getElementById('actionButtons').style.display = 'none';
|
| 399 |
-
document.getElementById('fileInputWrapper').style.display = 'block';
|
| 400 |
-
document.getElementById('processBtn').style.display = 'none';
|
| 401 |
-
}
|
| 402 |
-
|
| 403 |
-
async function processImage() {
|
| 404 |
-
if (!capturedImageData) {
|
| 405 |
-
alert('No image to process');
|
| 406 |
-
return;
|
| 407 |
-
}
|
| 408 |
-
|
| 409 |
-
// Show loading
|
| 410 |
-
document.getElementById('loading').style.display = 'flex';
|
| 411 |
-
document.getElementById('processBtn').style.display = 'none';
|
| 412 |
-
|
| 413 |
-
try {
|
| 414 |
-
const formData = new FormData();
|
| 415 |
-
formData.append('image', capturedImageData);
|
| 416 |
-
|
| 417 |
-
const response = await fetch('/api/upload', {
|
| 418 |
-
method: 'POST',
|
| 419 |
-
body: formData
|
| 420 |
-
});
|
| 421 |
-
|
| 422 |
-
const result = await response.json();
|
| 423 |
-
|
| 424 |
-
// Hide loading
|
| 425 |
-
document.getElementById('loading').style.display = 'none';
|
| 426 |
-
|
| 427 |
-
// Show results
|
| 428 |
-
displayResults(result);
|
| 429 |
-
|
| 430 |
-
} catch (error) {
|
| 431 |
-
console.error('Error processing image:', error);
|
| 432 |
-
document.getElementById('loading').style.display = 'none';
|
| 433 |
-
alert('Failed to process image. Please try again.');
|
| 434 |
-
document.getElementById('processBtn').style.display = 'block';
|
| 435 |
-
}
|
| 436 |
-
}
|
| 437 |
-
|
| 438 |
-
function displayResults(result) {
|
| 439 |
-
const resultsDiv = document.getElementById('results');
|
| 440 |
-
const contentDiv = document.getElementById('resultsContent');
|
| 441 |
-
|
| 442 |
-
if (!result.success) {
|
| 443 |
-
contentDiv.innerHTML = `
|
| 444 |
-
<div class="status-badge status-error">❌ Error</div>
|
| 445 |
-
<p><strong>Error:</strong> ${result.message}</p>
|
| 446 |
-
`;
|
| 447 |
-
resultsDiv.style.display = 'block';
|
| 448 |
-
return;
|
| 449 |
-
}
|
| 450 |
-
|
| 451 |
-
const data = result.data;
|
| 452 |
-
const statusClass = `status-${data.overall_status.replace('_', '-')}`;
|
| 453 |
-
const statusEmoji = getStatusEmoji(data.overall_status);
|
| 454 |
-
|
| 455 |
-
let html = `
|
| 456 |
-
<div class="status-badge ${statusClass}">
|
| 457 |
-
${statusEmoji} ${data.overall_status.replace('_', ' ').toUpperCase()}
|
| 458 |
-
</div>
|
| 459 |
-
<div class="metric">
|
| 460 |
-
<span>Processing Time:</span>
|
| 461 |
-
<strong>${data.processing_time_seconds}s</strong>
|
| 462 |
-
</div>
|
| 463 |
-
`;
|
| 464 |
-
|
| 465 |
-
// Add validation results
|
| 466 |
-
const validations = data.validations || {};
|
| 467 |
-
|
| 468 |
-
// Blur Detection
|
| 469 |
-
if (validations.blur_detection && !validations.blur_detection.error) {
|
| 470 |
-
const blur = validations.blur_detection;
|
| 471 |
-
html += `
|
| 472 |
-
<div class="validation-item ${blur.is_blurry ? 'error' : 'success'}">
|
| 473 |
-
<h4>🔍 Blur Detection</h4>
|
| 474 |
-
<div class="metric">
|
| 475 |
-
<span>Blur Score:</span>
|
| 476 |
-
<strong>${blur.blur_score}</strong>
|
| 477 |
-
</div>
|
| 478 |
-
<div class="metric">
|
| 479 |
-
<span>Quality:</span>
|
| 480 |
-
<strong>${blur.quality}</strong>
|
| 481 |
-
</div>
|
| 482 |
-
</div>
|
| 483 |
-
`;
|
| 484 |
-
}
|
| 485 |
-
|
| 486 |
-
// Brightness Validation
|
| 487 |
-
if (validations.brightness_validation && !validations.brightness_validation.error) {
|
| 488 |
-
const brightness = validations.brightness_validation;
|
| 489 |
-
html += `
|
| 490 |
-
<div class="validation-item ${brightness.has_brightness_issues ? 'error' : 'success'}">
|
| 491 |
-
<h4>💡 Brightness</h4>
|
| 492 |
-
<div class="metric">
|
| 493 |
-
<span>Mean Brightness:</span>
|
| 494 |
-
<strong>${brightness.mean_brightness}</strong>
|
| 495 |
-
</div>
|
| 496 |
-
<div class="metric">
|
| 497 |
-
<span>Quality Score:</span>
|
| 498 |
-
<strong>${(brightness.quality_score * 100).toFixed(1)}%</strong>
|
| 499 |
-
</div>
|
| 500 |
-
</div>
|
| 501 |
-
`;
|
| 502 |
-
}
|
| 503 |
-
|
| 504 |
-
// Resolution Check
|
| 505 |
-
if (validations.resolution_check && !validations.resolution_check.error) {
|
| 506 |
-
const resolution = validations.resolution_check;
|
| 507 |
-
html += `
|
| 508 |
-
<div class="validation-item ${resolution.meets_min_resolution ? 'success' : 'error'}">
|
| 509 |
-
<h4>📏 Resolution</h4>
|
| 510 |
-
<div class="metric">
|
| 511 |
-
<span>Dimensions:</span>
|
| 512 |
-
<strong>${resolution.width} × ${resolution.height}</strong>
|
| 513 |
-
</div>
|
| 514 |
-
<div class="metric">
|
| 515 |
-
<span>Megapixels:</span>
|
| 516 |
-
<strong>${resolution.megapixels} MP</strong>
|
| 517 |
-
</div>
|
| 518 |
-
<div class="metric">
|
| 519 |
-
<span>Quality Tier:</span>
|
| 520 |
-
<strong>${resolution.quality_tier}</strong>
|
| 521 |
-
</div>
|
| 522 |
-
</div>
|
| 523 |
-
`;
|
| 524 |
-
}
|
| 525 |
-
|
| 526 |
-
// Exposure Check
|
| 527 |
-
if (validations.exposure_check && !validations.exposure_check.error) {
|
| 528 |
-
const exposure = validations.exposure_check;
|
| 529 |
-
html += `
|
| 530 |
-
<div class="validation-item ${exposure.has_good_exposure ? 'success' : 'warning'}">
|
| 531 |
-
<h4>☀️ Exposure</h4>
|
| 532 |
-
<div class="metric">
|
| 533 |
-
<span>Quality:</span>
|
| 534 |
-
<strong>${exposure.exposure_quality}</strong>
|
| 535 |
-
</div>
|
| 536 |
-
<div class="metric">
|
| 537 |
-
<span>Dynamic Range:</span>
|
| 538 |
-
<strong>${exposure.dynamic_range}</strong>
|
| 539 |
-
</div>
|
| 540 |
-
</div>
|
| 541 |
-
`;
|
| 542 |
-
}
|
| 543 |
-
|
| 544 |
-
// Issues
|
| 545 |
-
if (data.issues && data.issues.length > 0) {
|
| 546 |
-
html += `
|
| 547 |
-
<div class="validation-item error">
|
| 548 |
-
<h4>⚠️ Issues Found</h4>
|
| 549 |
-
<ul class="issues-list">
|
| 550 |
-
`;
|
| 551 |
-
data.issues.forEach(issue => {
|
| 552 |
-
html += `<li><strong>${issue.type}:</strong> ${issue.message}</li>`;
|
| 553 |
-
});
|
| 554 |
-
html += `</ul></div>`;
|
| 555 |
-
}
|
| 556 |
-
|
| 557 |
-
// Recommendations
|
| 558 |
-
if (data.recommendations && data.recommendations.length > 0) {
|
| 559 |
-
html += `
|
| 560 |
-
<div class="validation-item">
|
| 561 |
-
<h4>💡 Recommendations</h4>
|
| 562 |
-
<ul class="issues-list">
|
| 563 |
-
`;
|
| 564 |
-
data.recommendations.forEach(rec => {
|
| 565 |
-
html += `<li>${rec}</li>`;
|
| 566 |
-
});
|
| 567 |
-
html += `</ul></div>`;
|
| 568 |
-
}
|
| 569 |
-
|
| 570 |
-
contentDiv.innerHTML = html;
|
| 571 |
-
resultsDiv.style.display = 'block';
|
| 572 |
-
}
|
| 573 |
-
|
| 574 |
-
function getStatusEmoji(status) {
|
| 575 |
-
const emojis = {
|
| 576 |
-
'excellent': '🌟',
|
| 577 |
-
'good': '✅',
|
| 578 |
-
'acceptable': '⚠️',
|
| 579 |
-
'needs_improvement': '📈',
|
| 580 |
-
'rejected': '❌',
|
| 581 |
-
'error': '💥'
|
| 582 |
-
};
|
| 583 |
-
return emojis[status] || '❓';
|
| 584 |
-
}
|
| 585 |
-
|
| 586 |
-
function resetApp() {
|
| 587 |
-
// Reset all states
|
| 588 |
-
capturedImageData = null;
|
| 589 |
-
|
| 590 |
-
// Hide all sections
|
| 591 |
-
document.getElementById('cameraPreview').style.display = 'none';
|
| 592 |
-
document.getElementById('actionButtons').style.display = 'none';
|
| 593 |
-
document.getElementById('processBtn').style.display = 'none';
|
| 594 |
-
document.getElementById('results').style.display = 'none';
|
| 595 |
-
document.getElementById('loading').style.display = 'none';
|
| 596 |
-
|
| 597 |
-
// Show file input
|
| 598 |
-
document.getElementById('fileInputWrapper').style.display = 'block';
|
| 599 |
-
|
| 600 |
-
// Reset captured image
|
| 601 |
-
const capturedImage = document.getElementById('capturedImage');
|
| 602 |
-
capturedImage.style.display = 'none';
|
| 603 |
-
capturedImage.src = '';
|
| 604 |
-
|
| 605 |
-
// Reset file input
|
| 606 |
-
document.getElementById('fileInput').value = '';
|
| 607 |
-
|
| 608 |
-
// Stop camera if running
|
| 609 |
-
stopCamera();
|
| 610 |
-
}
|
| 611 |
-
|
| 612 |
-
// Add button to start camera if supported
|
| 613 |
-
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
| 614 |
-
const startCameraBtn = document.createElement('button');
|
| 615 |
-
startCameraBtn.className = 'btn secondary';
|
| 616 |
-
startCameraBtn.innerHTML = '📹 Use Camera';
|
| 617 |
-
startCameraBtn.onclick = startCamera;
|
| 618 |
-
|
| 619 |
-
const fileWrapper = document.getElementById('fileInputWrapper');
|
| 620 |
-
fileWrapper.parentNode.insertBefore(startCameraBtn, fileWrapper.nextSibling);
|
| 621 |
-
}
|
| 622 |
-
</script>
|
| 623 |
-
</body>
|
| 624 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
templates/redoc.html
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>PhotoGuard API Reference - ReDoc</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
}
|
| 12 |
+
</style>
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<redoc spec-url="/api/openapi.json"
|
| 16 |
+
hide-download-button="false"
|
| 17 |
+
suppress-warnings="true"
|
| 18 |
+
scroll-y-offset="0"
|
| 19 |
+
native-scrollbars="true"
|
| 20 |
+
theme='{
|
| 21 |
+
"colors": {
|
| 22 |
+
"primary": {
|
| 23 |
+
"main": "#000000"
|
| 24 |
+
},
|
| 25 |
+
"success": {
|
| 26 |
+
"main": "#2d2d2d"
|
| 27 |
+
},
|
| 28 |
+
"text": {
|
| 29 |
+
"primary": "#1a1a1a",
|
| 30 |
+
"secondary": "#6b6b6b"
|
| 31 |
+
},
|
| 32 |
+
"http": {
|
| 33 |
+
"get": "#6b6b6b",
|
| 34 |
+
"post": "#000000",
|
| 35 |
+
"put": "#2d2d2d",
|
| 36 |
+
"delete": "#8b0000"
|
| 37 |
+
}
|
| 38 |
+
},
|
| 39 |
+
"typography": {
|
| 40 |
+
"fontSize": "14px",
|
| 41 |
+
"lineHeight": "1.6em",
|
| 42 |
+
"fontFamily": "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif",
|
| 43 |
+
"headings": {
|
| 44 |
+
"fontFamily": "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif",
|
| 45 |
+
"fontWeight": "600"
|
| 46 |
+
},
|
| 47 |
+
"code": {
|
| 48 |
+
"fontSize": "13px",
|
| 49 |
+
"fontFamily": "\"Source Code Pro\", Monaco, Consolas, monospace",
|
| 50 |
+
"backgroundColor": "#f5f5f5",
|
| 51 |
+
"color": "#1a1a1a"
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"sidebar": {
|
| 55 |
+
"backgroundColor": "#ffffff",
|
| 56 |
+
"textColor": "#1a1a1a",
|
| 57 |
+
"activeTextColor": "#000000",
|
| 58 |
+
"groupItems": {
|
| 59 |
+
"textTransform": "uppercase"
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
"rightPanel": {
|
| 63 |
+
"backgroundColor": "#1a1a1a",
|
| 64 |
+
"textColor": "#f5f5f5"
|
| 65 |
+
}
|
| 66 |
+
}'>
|
| 67 |
+
</redoc>
|
| 68 |
+
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
|
| 69 |
+
</body>
|
| 70 |
+
</html>
|
templates/swagger.html
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>PhotoGuard API Documentation - Swagger UI</title>
|
| 7 |
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.10.5/swagger-ui.css">
|
| 8 |
+
<style>
|
| 9 |
+
body {
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
background-color: #f5f5f5;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
.topbar {
|
| 16 |
+
background-color: #000000 !important;
|
| 17 |
+
padding: 10px 0;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.topbar-wrapper .link {
|
| 21 |
+
color: #ffffff !important;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.swagger-ui .info .title {
|
| 25 |
+
color: #1a1a1a;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.swagger-ui .opblock.opblock-post {
|
| 29 |
+
border-color: #000000;
|
| 30 |
+
background: rgba(0, 0, 0, 0.05);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.swagger-ui .opblock.opblock-post .opblock-summary-method {
|
| 34 |
+
background: #000000;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.swagger-ui .opblock.opblock-get {
|
| 38 |
+
border-color: #6b6b6b;
|
| 39 |
+
background: rgba(107, 107, 107, 0.05);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.swagger-ui .opblock.opblock-get .opblock-summary-method {
|
| 43 |
+
background: #6b6b6b;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.swagger-ui .opblock-tag {
|
| 47 |
+
border-bottom: 2px solid #cccccc;
|
| 48 |
+
color: #1a1a1a;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.swagger-ui .btn.execute {
|
| 52 |
+
background-color: #000000;
|
| 53 |
+
border-color: #000000;
|
| 54 |
+
color: #ffffff;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.swagger-ui .btn.execute:hover {
|
| 58 |
+
background-color: #2d2d2d;
|
| 59 |
+
border-color: #2d2d2d;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.swagger-ui .scheme-container {
|
| 63 |
+
background-color: #ffffff;
|
| 64 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
| 65 |
+
}
|
| 66 |
+
</style>
|
| 67 |
+
</head>
|
| 68 |
+
<body>
|
| 69 |
+
<div id="swagger-ui"></div>
|
| 70 |
+
<script src="https://unpkg.com/swagger-ui-dist@5.10.5/swagger-ui-bundle.js"></script>
|
| 71 |
+
<script src="https://unpkg.com/swagger-ui-dist@5.10.5/swagger-ui-standalone-preset.js"></script>
|
| 72 |
+
<script>
|
| 73 |
+
window.onload = function() {
|
| 74 |
+
window.ui = SwaggerUIBundle({
|
| 75 |
+
url: "/api/openapi.json",
|
| 76 |
+
dom_id: '#swagger-ui',
|
| 77 |
+
deepLinking: true,
|
| 78 |
+
presets: [
|
| 79 |
+
SwaggerUIBundle.presets.apis,
|
| 80 |
+
SwaggerUIStandalonePreset
|
| 81 |
+
],
|
| 82 |
+
plugins: [
|
| 83 |
+
SwaggerUIBundle.plugins.DownloadUrl
|
| 84 |
+
],
|
| 85 |
+
layout: "StandaloneLayout",
|
| 86 |
+
defaultModelsExpandDepth: 1,
|
| 87 |
+
defaultModelExpandDepth: 3,
|
| 88 |
+
docExpansion: "list",
|
| 89 |
+
filter: true,
|
| 90 |
+
showRequestHeaders: true,
|
| 91 |
+
tryItOutEnabled: true
|
| 92 |
+
});
|
| 93 |
+
};
|
| 94 |
+
</script>
|
| 95 |
+
</body>
|
| 96 |
+
</html>
|
tests/test_api_endpoints.py
CHANGED
|
@@ -1,24 +1,63 @@
|
|
|
|
|
|
|
|
| 1 |
import pytest
|
| 2 |
-
from
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
|
| 5 |
@pytest.fixture
|
| 6 |
-
def client():
|
| 7 |
-
"""
|
| 8 |
-
app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
with app.test_client() as client:
|
| 10 |
yield client
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
response = client.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
assert response.status_code == 400
|
| 16 |
-
|
| 17 |
-
assert '
|
| 18 |
-
assert
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
|
| 3 |
import pytest
|
| 4 |
+
from PIL import Image
|
| 5 |
+
|
| 6 |
+
from app import create_app
|
| 7 |
+
|
| 8 |
|
| 9 |
@pytest.fixture
|
| 10 |
+
def client(tmp_path):
|
| 11 |
+
"""Configure an isolated Flask test client per test run."""
|
| 12 |
+
app = create_app('testing')
|
| 13 |
+
|
| 14 |
+
upload_dir = tmp_path / 'uploads'
|
| 15 |
+
processed_dir = tmp_path / 'processed'
|
| 16 |
+
rejected_dir = tmp_path / 'rejected'
|
| 17 |
+
for directory in (upload_dir, processed_dir, rejected_dir):
|
| 18 |
+
directory.mkdir(parents=True, exist_ok=True)
|
| 19 |
+
|
| 20 |
+
app.config.update({
|
| 21 |
+
'UPLOAD_FOLDER': str(upload_dir),
|
| 22 |
+
'PROCESSED_FOLDER': str(processed_dir),
|
| 23 |
+
'REJECTED_FOLDER': str(rejected_dir),
|
| 24 |
+
})
|
| 25 |
+
|
| 26 |
with app.test_client() as client:
|
| 27 |
yield client
|
| 28 |
|
| 29 |
+
|
| 30 |
+
def test_health_endpoint(client):
|
| 31 |
+
response = client.get('/api/health')
|
| 32 |
+
assert response.status_code == 200
|
| 33 |
+
payload = response.get_json()
|
| 34 |
+
assert payload['success'] is True
|
| 35 |
+
assert payload['data']['status'] == 'healthy'
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_validate_endpoint_without_file(client):
|
| 39 |
+
response = client.post('/api/validate')
|
| 40 |
assert response.status_code == 400
|
| 41 |
+
payload = response.get_json()
|
| 42 |
+
assert payload['success'] is False
|
| 43 |
+
assert 'No image file provided' in payload['message']
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_validate_endpoint_with_generated_image(client):
|
| 47 |
+
image = Image.new('RGB', (1024, 768), color=(180, 180, 180))
|
| 48 |
+
buffer = io.BytesIO()
|
| 49 |
+
image.save(buffer, format='JPEG')
|
| 50 |
+
buffer.seek(0)
|
| 51 |
+
|
| 52 |
+
response = client.post(
|
| 53 |
+
'/api/validate',
|
| 54 |
+
data={'image': (buffer, 'sample.jpg')},
|
| 55 |
+
content_type='multipart/form-data'
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Validation may return 200 for pass/fail; ensure we get a structured payload.
|
| 59 |
+
assert response.status_code == 200
|
| 60 |
+
payload = response.get_json()
|
| 61 |
+
assert payload['success'] is True
|
| 62 |
+
assert 'summary' in payload['data']
|
| 63 |
+
assert 'checks' in payload['data']
|