niru-nny commited on
Commit
27b442c
·
1 Parent(s): 2a102e3

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 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
- storage/temp/*
79
- storage/processed/*
80
- storage/rejected/*
81
- !storage/temp/.gitkeep
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 models
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 curl -f http://localhost:8000/api/health || exit 1
74
 
75
  # Production server command
76
- CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "app:app"]
 
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
- # Civic Quality Control API
2
 
3
- A production-ready mobile photo validation system for civic documentation with intelligent quality control and comprehensive API endpoints.
4
 
5
  ## 🚀 Key Features
6
 
@@ -49,18 +49,20 @@ pip install -r requirements.txt
49
  ### Setup & Run
50
 
51
  ```bash
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
- # Access mobile interface
60
- # http://localhost:5000/mobile_upload.html
61
  ```
62
 
63
- ## 📱 API Endpoints
 
 
 
 
 
 
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
- ├── metadata_extraction.py
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**: 2.0
378
- **Last Updated**: September 25, 2025
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
- # Load configuration
11
- from config import config
12
- app.config.from_object(config[config_name])
 
 
 
 
13
 
14
- # Enable CORS if available
 
 
 
 
 
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
- # Create necessary directories
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 blueprints
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, request, jsonify, current_app, render_template, send_from_directory
2
  import os
3
  import uuid
4
- from werkzeug.utils import secure_filename
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
- @upload_bp.route('/upload', methods=['POST'])
18
- def upload_image():
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
- @upload_bp.route('/validate-url', methods=['POST'])
64
- def validate_image_url():
65
- """Validate image from URL endpoint."""
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
- @upload_bp.route('/mobile', methods=['GET'])
96
- def mobile_interface():
97
- """Serve mobile-friendly upload interface."""
98
- return render_template('mobile_upload.html')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Check if file is in request
108
- if 'image' not in request.files:
109
- return ResponseFormatter.error("No image file provided", 400)
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['overall_status'],
145
- "overall_score": validation_results['overall_score'],
146
- "issues_found": validation_results['issues_found'],
147
- "recommendations": validation_results['recommendations']
148
  },
149
- "checks": validation_results['checks']
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 e:
160
- return ResponseFormatter.error(f"Validation failed: {str(e)}", 500)
 
161
 
162
  @upload_bp.route('/validation-rules', methods=['GET'])
163
  def get_validation_rules():
164
- """Get current validation rules."""
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
- @upload_bp.route('/test-api', methods=['GET'])
174
- def test_api_endpoint():
175
- """Test API endpoint with sample data."""
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
- """Get processing statistics and summary."""
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
- except Exception as e:
265
- return ResponseFormatter.error(f"Failed to get summary: {str(e)}", 500)
266
 
267
  @upload_bp.route('/health', methods=['GET'])
268
  def health_check():
269
- """Health check endpoint."""
270
  return ResponseFormatter.success(
271
  data={
272
- "status": "healthy",
273
- "service": "civic-quality-control",
274
- "api_version": "2.0",
275
  "validation_rules": "updated"
276
  },
277
- message="Service is running with updated validation rules"
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)}") # 6. Object Detection (if available)
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
- # Override with stricter settings if needed
194
- # Example: Require HTTPS in production
195
- # SESSION_COOKIE_SECURE = True
196
- # SESSION_COOKIE_HTTPONLY = True
 
 
 
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
- Flask==2.3.3
2
- Flask-CORS==4.0.0
3
- opencv-python==4.8.1.78
4
- Pillow==10.0.1
5
- numpy==1.24.3
6
- ultralytics==8.0.196
7
- python-dotenv==1.0.0
8
- pytest==7.4.2
9
- requests==2.31.0
 
 
10
  piexif==1.1.3
11
- geopy==2.4.0
12
- gunicorn==21.2.0
13
- PyYAML==6.0.1
 
 
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 flask import Flask
3
- from app import app # Import the Flask app
 
 
4
 
5
  @pytest.fixture
6
- def client():
7
- """Test client for the Flask app."""
8
- app.config['TESTING'] = True
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  with app.test_client() as client:
10
  yield client
11
 
12
- def test_check_quality_no_image(client):
13
- """Test check_quality endpoint with no image."""
14
- response = client.post('/check_quality')
 
 
 
 
 
 
 
 
15
  assert response.status_code == 400
16
- data = response.get_json()
17
- assert 'error' in data
18
- assert data['error'] == 'No image uploaded'
19
-
20
- def test_check_quality_with_image(client, sample_image):
21
- """Test check_quality endpoint with a sample image."""
22
- # This would need actual image data
23
- # For now, just test the structure
24
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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']