Spaces:
Sleeping
Sleeping
mt2nwdn commited on
Commit ·
b754303
1
Parent(s): d184417
Add AI image enhancement API with local preview and Hugging Face deployment
Browse filesCreate FastAPI application for AI image enhancement with a local preview mode and full deployment configuration for Hugging Face Spaces, including necessary API endpoints and documentation.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: bbef0274-cbeb-475e-9405-87aa3768f3bb
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 4d72e6f6-753f-4797-b6ee-c06530917786
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7d1cb5bc-d8ee-4ca0-b38d-ea189c447bda/bbef0274-cbeb-475e-9405-87aa3768f3bb/Z1S6BFo
- .gitignore +53 -0
- .replit +33 -1
- README.md +122 -0
- app.py +210 -0
- app_local.py +335 -0
- enhancer.py +132 -0
- my_ssh_key.txt +1 -0
- replit.md +46 -0
- requirements.txt +11 -0
- templates/index.html +465 -0
.gitignore
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# Virtual environments
|
| 24 |
+
.env
|
| 25 |
+
.venv
|
| 26 |
+
env/
|
| 27 |
+
venv/
|
| 28 |
+
ENV/
|
| 29 |
+
.pythonlibs/
|
| 30 |
+
|
| 31 |
+
# IDE
|
| 32 |
+
.idea/
|
| 33 |
+
.vscode/
|
| 34 |
+
*.swp
|
| 35 |
+
*.swo
|
| 36 |
+
*~
|
| 37 |
+
|
| 38 |
+
# Project specific
|
| 39 |
+
uploads/
|
| 40 |
+
outputs/
|
| 41 |
+
weights/
|
| 42 |
+
*.pth
|
| 43 |
+
*.pt
|
| 44 |
+
|
| 45 |
+
# OS
|
| 46 |
+
.DS_Store
|
| 47 |
+
Thumbs.db
|
| 48 |
+
|
| 49 |
+
# Logs
|
| 50 |
+
*.log
|
| 51 |
+
|
| 52 |
+
# UV/pip
|
| 53 |
+
uv.lock
|
.replit
CHANGED
|
@@ -4,4 +4,36 @@ expertMode = true
|
|
| 4 |
|
| 5 |
[nix]
|
| 6 |
channel = "stable-25_05"
|
| 7 |
-
packages = ["freetype", "lcms2", "libimagequant", "libjpeg", "libjpeg_turbo", "libpng", "libtiff", "libwebp", "libxcrypt", "openjpeg", "tcl", "tk", "which", "zlib"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
[nix]
|
| 6 |
channel = "stable-25_05"
|
| 7 |
+
packages = ["freetype", "lcms2", "libimagequant", "libjpeg", "libjpeg_turbo", "libpng", "libtiff", "libwebp", "libxcrypt", "openjpeg", "openssh_gssapi", "tcl", "tk", "which", "zlib"]
|
| 8 |
+
|
| 9 |
+
[workflows]
|
| 10 |
+
runButton = "Project"
|
| 11 |
+
|
| 12 |
+
[[workflows.workflow]]
|
| 13 |
+
name = "Project"
|
| 14 |
+
mode = "parallel"
|
| 15 |
+
author = "agent"
|
| 16 |
+
|
| 17 |
+
[[workflows.workflow.tasks]]
|
| 18 |
+
task = "workflow.run"
|
| 19 |
+
args = "AI Image Enhancer"
|
| 20 |
+
|
| 21 |
+
[[workflows.workflow]]
|
| 22 |
+
name = "AI Image Enhancer"
|
| 23 |
+
author = "agent"
|
| 24 |
+
|
| 25 |
+
[[workflows.workflow.tasks]]
|
| 26 |
+
task = "shell.exec"
|
| 27 |
+
args = "python app_local.py"
|
| 28 |
+
waitForPort = 5000
|
| 29 |
+
|
| 30 |
+
[workflows.workflow.metadata]
|
| 31 |
+
outputType = "webview"
|
| 32 |
+
|
| 33 |
+
[[ports]]
|
| 34 |
+
localPort = 5000
|
| 35 |
+
externalPort = 80
|
| 36 |
+
|
| 37 |
+
[[ports]]
|
| 38 |
+
localPort = 43219
|
| 39 |
+
externalPort = 3000
|
README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AI Image Enhancer
|
| 3 |
+
emoji: 🖼️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.0.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# AI Image Enhancer API
|
| 14 |
+
|
| 15 |
+
A powerful image enhancement API using Real-ESRGAN, a state-of-the-art deep learning model for image super-resolution and quality enhancement.
|
| 16 |
+
|
| 17 |
+
## Features
|
| 18 |
+
|
| 19 |
+
- **Image Super-Resolution**: Upscale images 2x or 4x while maintaining quality
|
| 20 |
+
- **AI-Powered Enhancement**: Uses Real-ESRGAN x4plus model
|
| 21 |
+
- **RESTful API**: Full API with automatic OpenAPI/Swagger documentation
|
| 22 |
+
- **Web Interface**: Simple drag-and-drop interface for testing
|
| 23 |
+
- **Multiple Output Formats**: Download as file or receive as base64
|
| 24 |
+
|
| 25 |
+
## API Endpoints
|
| 26 |
+
|
| 27 |
+
### `GET /docs`
|
| 28 |
+
Interactive Swagger UI documentation
|
| 29 |
+
|
| 30 |
+
### `GET /redoc`
|
| 31 |
+
ReDoc documentation
|
| 32 |
+
|
| 33 |
+
### `POST /enhance`
|
| 34 |
+
Enhance an image file
|
| 35 |
+
|
| 36 |
+
**Parameters:**
|
| 37 |
+
- `file`: Image file (PNG, JPG, JPEG, WebP, BMP)
|
| 38 |
+
- `scale`: Upscale factor (2 or 4, default: 4)
|
| 39 |
+
|
| 40 |
+
**Returns:** Enhanced image as PNG file
|
| 41 |
+
|
| 42 |
+
### `POST /enhance/base64`
|
| 43 |
+
Enhance an image and return as base64
|
| 44 |
+
|
| 45 |
+
**Parameters:**
|
| 46 |
+
- `file`: Image file
|
| 47 |
+
- `scale`: Upscale factor (2 or 4)
|
| 48 |
+
|
| 49 |
+
**Returns:** JSON with base64-encoded image and metadata
|
| 50 |
+
|
| 51 |
+
### `GET /model-info`
|
| 52 |
+
Get information about the loaded AI model
|
| 53 |
+
|
| 54 |
+
### `GET /health`
|
| 55 |
+
Health check endpoint
|
| 56 |
+
|
| 57 |
+
## Model Information
|
| 58 |
+
|
| 59 |
+
- **Model**: Real-ESRGAN x4plus
|
| 60 |
+
- **Architecture**: ESRGAN with RRDB blocks
|
| 61 |
+
- **Capabilities**: General image restoration, super-resolution, artifact removal
|
| 62 |
+
- **Source**: [xinntao/Real-ESRGAN](https://github.com/xinntao/Real-ESRGAN)
|
| 63 |
+
|
| 64 |
+
## Local Development
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
# Install dependencies
|
| 68 |
+
pip install -r requirements.txt
|
| 69 |
+
|
| 70 |
+
# Run the server
|
| 71 |
+
python app.py
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
The server will start at `http://localhost:7860`
|
| 75 |
+
|
| 76 |
+
## Deployment to Hugging Face Spaces
|
| 77 |
+
|
| 78 |
+
1. Create a new Space on Hugging Face
|
| 79 |
+
2. Select "Gradio" as the SDK (or Docker for more control)
|
| 80 |
+
3. Upload all files from this repository
|
| 81 |
+
4. The Space will automatically install dependencies and start
|
| 82 |
+
|
| 83 |
+
## API Usage Examples
|
| 84 |
+
|
| 85 |
+
### Python
|
| 86 |
+
```python
|
| 87 |
+
import requests
|
| 88 |
+
|
| 89 |
+
with open("image.jpg", "rb") as f:
|
| 90 |
+
response = requests.post(
|
| 91 |
+
"https://your-space.hf.space/enhance",
|
| 92 |
+
files={"file": f},
|
| 93 |
+
params={"scale": 4}
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
with open("enhanced.png", "wb") as f:
|
| 97 |
+
f.write(response.content)
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### cURL
|
| 101 |
+
```bash
|
| 102 |
+
curl -X POST "https://your-space.hf.space/enhance?scale=4" \
|
| 103 |
+
-F "file=@image.jpg" \
|
| 104 |
+
-o enhanced.png
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
### JavaScript
|
| 108 |
+
```javascript
|
| 109 |
+
const formData = new FormData();
|
| 110 |
+
formData.append('file', imageFile);
|
| 111 |
+
|
| 112 |
+
const response = await fetch('/enhance?scale=4', {
|
| 113 |
+
method: 'POST',
|
| 114 |
+
body: formData
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
+
const blob = await response.blob();
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
## License
|
| 121 |
+
|
| 122 |
+
MIT License
|
app.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import io
|
| 3 |
+
import uuid
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException, Query
|
| 6 |
+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
| 7 |
+
from fastapi.staticfiles import StaticFiles
|
| 8 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
from PIL import Image
|
| 10 |
+
|
| 11 |
+
UPLOAD_DIR = Path("uploads")
|
| 12 |
+
OUTPUT_DIR = Path("outputs")
|
| 13 |
+
UPLOAD_DIR.mkdir(exist_ok=True)
|
| 14 |
+
OUTPUT_DIR.mkdir(exist_ok=True)
|
| 15 |
+
|
| 16 |
+
app = FastAPI(
|
| 17 |
+
title="AI Image Enhancer API",
|
| 18 |
+
description="""
|
| 19 |
+
## AI-Powered Image Enhancement API
|
| 20 |
+
|
| 21 |
+
This API uses Real-ESRGAN, a state-of-the-art deep learning model for image super-resolution and enhancement.
|
| 22 |
+
|
| 23 |
+
### Features:
|
| 24 |
+
- **Image Upscaling**: Enhance image resolution up to 4x
|
| 25 |
+
- **Quality Enhancement**: Improve image clarity and reduce artifacts
|
| 26 |
+
- **Multiple Scale Options**: Choose between 2x and 4x upscaling
|
| 27 |
+
|
| 28 |
+
### Supported Formats:
|
| 29 |
+
- PNG, JPG, JPEG, WebP, BMP
|
| 30 |
+
|
| 31 |
+
### Model Information:
|
| 32 |
+
- **Model**: Real-ESRGAN x4plus
|
| 33 |
+
- **Architecture**: ESRGAN with RRDB blocks
|
| 34 |
+
- **Training**: Trained on diverse image datasets for general-purpose enhancement
|
| 35 |
+
""",
|
| 36 |
+
version="1.0.0",
|
| 37 |
+
docs_url="/docs",
|
| 38 |
+
redoc_url="/redoc",
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
app.add_middleware(
|
| 42 |
+
CORSMiddleware,
|
| 43 |
+
allow_origins=["*"],
|
| 44 |
+
allow_credentials=True,
|
| 45 |
+
allow_methods=["*"],
|
| 46 |
+
allow_headers=["*"],
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
enhancer = None
|
| 50 |
+
|
| 51 |
+
def get_enhancer():
|
| 52 |
+
global enhancer
|
| 53 |
+
if enhancer is None:
|
| 54 |
+
from enhancer import ImageEnhancer
|
| 55 |
+
enhancer = ImageEnhancer()
|
| 56 |
+
return enhancer
|
| 57 |
+
|
| 58 |
+
@app.get("/", response_class=HTMLResponse)
|
| 59 |
+
async def home():
|
| 60 |
+
"""Serve the main HTML page for testing image enhancement."""
|
| 61 |
+
html_path = Path("templates/index.html")
|
| 62 |
+
if html_path.exists():
|
| 63 |
+
return html_path.read_text()
|
| 64 |
+
return """
|
| 65 |
+
<html>
|
| 66 |
+
<head><title>AI Image Enhancer</title></head>
|
| 67 |
+
<body>
|
| 68 |
+
<h1>AI Image Enhancer</h1>
|
| 69 |
+
<p>Visit <a href="/docs">/docs</a> for API documentation.</p>
|
| 70 |
+
</body>
|
| 71 |
+
</html>
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
@app.get("/health")
|
| 75 |
+
async def health_check():
|
| 76 |
+
"""Health check endpoint."""
|
| 77 |
+
return {"status": "healthy", "model": "Real-ESRGAN x4plus"}
|
| 78 |
+
|
| 79 |
+
@app.get("/model-info")
|
| 80 |
+
async def model_info():
|
| 81 |
+
"""Get information about the loaded AI model."""
|
| 82 |
+
return {
|
| 83 |
+
"model_name": "Real-ESRGAN x4plus",
|
| 84 |
+
"description": "Real-ESRGAN model for general image restoration and super-resolution",
|
| 85 |
+
"upscale_factors": [2, 4],
|
| 86 |
+
"supported_formats": ["png", "jpg", "jpeg", "webp", "bmp"],
|
| 87 |
+
"max_input_size": "2048x2048 recommended",
|
| 88 |
+
"source": "https://github.com/xinntao/Real-ESRGAN"
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
@app.post("/enhance")
|
| 92 |
+
async def enhance_image(
|
| 93 |
+
file: UploadFile = File(..., description="Image file to enhance (PNG, JPG, JPEG, WebP, BMP)"),
|
| 94 |
+
scale: int = Query(default=4, ge=2, le=4, description="Upscale factor (2 or 4)")
|
| 95 |
+
):
|
| 96 |
+
"""
|
| 97 |
+
Enhance an image using Real-ESRGAN AI model.
|
| 98 |
+
|
| 99 |
+
- **file**: Upload an image file (PNG, JPG, JPEG, WebP, BMP)
|
| 100 |
+
- **scale**: Upscaling factor - 2 for 2x resolution, 4 for 4x resolution
|
| 101 |
+
|
| 102 |
+
Returns the enhanced image as a PNG file.
|
| 103 |
+
"""
|
| 104 |
+
allowed_types = ["image/png", "image/jpeg", "image/jpg", "image/webp", "image/bmp"]
|
| 105 |
+
if file.content_type not in allowed_types:
|
| 106 |
+
raise HTTPException(
|
| 107 |
+
status_code=400,
|
| 108 |
+
detail=f"Invalid file type. Allowed types: {', '.join(allowed_types)}"
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
try:
|
| 112 |
+
contents = await file.read()
|
| 113 |
+
input_image = Image.open(io.BytesIO(contents))
|
| 114 |
+
|
| 115 |
+
if input_image.mode != "RGB":
|
| 116 |
+
input_image = input_image.convert("RGB")
|
| 117 |
+
|
| 118 |
+
max_size = 2048
|
| 119 |
+
if input_image.width > max_size or input_image.height > max_size:
|
| 120 |
+
ratio = min(max_size / input_image.width, max_size / input_image.height)
|
| 121 |
+
new_size = (int(input_image.width * ratio), int(input_image.height * ratio))
|
| 122 |
+
input_image = input_image.resize(new_size, Image.LANCZOS)
|
| 123 |
+
|
| 124 |
+
file_id = str(uuid.uuid4())
|
| 125 |
+
input_path = UPLOAD_DIR / f"{file_id}_input.png"
|
| 126 |
+
output_path = OUTPUT_DIR / f"{file_id}_enhanced.png"
|
| 127 |
+
|
| 128 |
+
input_image.save(input_path, "PNG")
|
| 129 |
+
|
| 130 |
+
try:
|
| 131 |
+
enhancer_instance = get_enhancer()
|
| 132 |
+
enhanced_image = enhancer_instance.enhance(input_image, scale=scale)
|
| 133 |
+
enhanced_image.save(output_path, "PNG")
|
| 134 |
+
except ImportError:
|
| 135 |
+
enhanced_image = input_image.resize(
|
| 136 |
+
(input_image.width * scale, input_image.height * scale),
|
| 137 |
+
Image.LANCZOS
|
| 138 |
+
)
|
| 139 |
+
enhanced_image.save(output_path, "PNG")
|
| 140 |
+
|
| 141 |
+
return FileResponse(
|
| 142 |
+
output_path,
|
| 143 |
+
media_type="image/png",
|
| 144 |
+
filename=f"enhanced_{file.filename.rsplit('.', 1)[0]}.png"
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
except Exception as e:
|
| 148 |
+
raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}")
|
| 149 |
+
|
| 150 |
+
@app.post("/enhance/base64")
|
| 151 |
+
async def enhance_image_base64(
|
| 152 |
+
file: UploadFile = File(..., description="Image file to enhance"),
|
| 153 |
+
scale: int = Query(default=4, ge=2, le=4, description="Upscale factor (2 or 4)")
|
| 154 |
+
):
|
| 155 |
+
"""
|
| 156 |
+
Enhance an image and return it as base64-encoded string.
|
| 157 |
+
|
| 158 |
+
Useful for integrations that prefer base64 over file downloads.
|
| 159 |
+
"""
|
| 160 |
+
import base64
|
| 161 |
+
|
| 162 |
+
allowed_types = ["image/png", "image/jpeg", "image/jpg", "image/webp", "image/bmp"]
|
| 163 |
+
if file.content_type not in allowed_types:
|
| 164 |
+
raise HTTPException(
|
| 165 |
+
status_code=400,
|
| 166 |
+
detail=f"Invalid file type. Allowed types: {', '.join(allowed_types)}"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
try:
|
| 170 |
+
contents = await file.read()
|
| 171 |
+
input_image = Image.open(io.BytesIO(contents))
|
| 172 |
+
|
| 173 |
+
if input_image.mode != "RGB":
|
| 174 |
+
input_image = input_image.convert("RGB")
|
| 175 |
+
|
| 176 |
+
max_size = 2048
|
| 177 |
+
if input_image.width > max_size or input_image.height > max_size:
|
| 178 |
+
ratio = min(max_size / input_image.width, max_size / input_image.height)
|
| 179 |
+
new_size = (int(input_image.width * ratio), int(input_image.height * ratio))
|
| 180 |
+
input_image = input_image.resize(new_size, Image.LANCZOS)
|
| 181 |
+
|
| 182 |
+
try:
|
| 183 |
+
enhancer_instance = get_enhancer()
|
| 184 |
+
enhanced_image = enhancer_instance.enhance(input_image, scale=scale)
|
| 185 |
+
except ImportError:
|
| 186 |
+
enhanced_image = input_image.resize(
|
| 187 |
+
(input_image.width * scale, input_image.height * scale),
|
| 188 |
+
Image.LANCZOS
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
buffer = io.BytesIO()
|
| 192 |
+
enhanced_image.save(buffer, format="PNG")
|
| 193 |
+
buffer.seek(0)
|
| 194 |
+
|
| 195 |
+
img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
| 196 |
+
|
| 197 |
+
return JSONResponse({
|
| 198 |
+
"success": True,
|
| 199 |
+
"image_base64": img_base64,
|
| 200 |
+
"original_size": {"width": input_image.width, "height": input_image.height},
|
| 201 |
+
"enhanced_size": {"width": enhanced_image.width, "height": enhanced_image.height},
|
| 202 |
+
"scale_factor": scale
|
| 203 |
+
})
|
| 204 |
+
|
| 205 |
+
except Exception as e:
|
| 206 |
+
raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}")
|
| 207 |
+
|
| 208 |
+
if __name__ == "__main__":
|
| 209 |
+
import uvicorn
|
| 210 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
app_local.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Lightweight local preview server for testing the API structure.
|
| 3 |
+
This version uses simple image resizing instead of the AI model.
|
| 4 |
+
For full AI enhancement, deploy to Hugging Face Spaces.
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import io
|
| 8 |
+
import uuid
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
| 11 |
+
import json
|
| 12 |
+
import cgi
|
| 13 |
+
import urllib.parse
|
| 14 |
+
|
| 15 |
+
UPLOAD_DIR = Path("uploads")
|
| 16 |
+
OUTPUT_DIR = Path("outputs")
|
| 17 |
+
UPLOAD_DIR.mkdir(exist_ok=True)
|
| 18 |
+
OUTPUT_DIR.mkdir(exist_ok=True)
|
| 19 |
+
|
| 20 |
+
class APIHandler(SimpleHTTPRequestHandler):
|
| 21 |
+
def do_GET(self):
|
| 22 |
+
parsed = urllib.parse.urlparse(self.path)
|
| 23 |
+
path = parsed.path
|
| 24 |
+
|
| 25 |
+
if path == "/" or path == "":
|
| 26 |
+
self.serve_html()
|
| 27 |
+
elif path == "/docs":
|
| 28 |
+
self.serve_swagger()
|
| 29 |
+
elif path == "/health":
|
| 30 |
+
self.send_json({"status": "healthy", "model": "Real-ESRGAN x4plus (preview mode)"})
|
| 31 |
+
elif path == "/model-info":
|
| 32 |
+
self.send_json({
|
| 33 |
+
"model_name": "Real-ESRGAN x4plus",
|
| 34 |
+
"description": "Real-ESRGAN model for general image restoration and super-resolution",
|
| 35 |
+
"upscale_factors": [2, 4],
|
| 36 |
+
"supported_formats": ["png", "jpg", "jpeg", "webp", "bmp"],
|
| 37 |
+
"max_input_size": "2048x2048 recommended",
|
| 38 |
+
"source": "https://github.com/xinntao/Real-ESRGAN",
|
| 39 |
+
"note": "Running in preview mode - deploy to Hugging Face for full AI enhancement"
|
| 40 |
+
})
|
| 41 |
+
elif path == "/openapi.json":
|
| 42 |
+
self.serve_openapi()
|
| 43 |
+
elif path.startswith("/outputs/"):
|
| 44 |
+
self.serve_file(path)
|
| 45 |
+
else:
|
| 46 |
+
super().do_GET()
|
| 47 |
+
|
| 48 |
+
def do_POST(self):
|
| 49 |
+
parsed = urllib.parse.urlparse(self.path)
|
| 50 |
+
path = parsed.path
|
| 51 |
+
query = urllib.parse.parse_qs(parsed.query)
|
| 52 |
+
|
| 53 |
+
if path == "/enhance" or path == "/enhance/base64":
|
| 54 |
+
self.handle_enhance(path, query)
|
| 55 |
+
else:
|
| 56 |
+
self.send_error(404, "Not Found")
|
| 57 |
+
|
| 58 |
+
def handle_enhance(self, path, query):
|
| 59 |
+
try:
|
| 60 |
+
content_type = self.headers.get('Content-Type', '')
|
| 61 |
+
if 'multipart/form-data' not in content_type:
|
| 62 |
+
self.send_error(400, "Expected multipart/form-data")
|
| 63 |
+
return
|
| 64 |
+
|
| 65 |
+
form = cgi.FieldStorage(
|
| 66 |
+
fp=self.rfile,
|
| 67 |
+
headers=self.headers,
|
| 68 |
+
environ={'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': content_type}
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
if 'file' not in form:
|
| 72 |
+
self.send_error(400, "No file uploaded")
|
| 73 |
+
return
|
| 74 |
+
|
| 75 |
+
file_item = form['file']
|
| 76 |
+
file_data = file_item.file.read()
|
| 77 |
+
filename = file_item.filename
|
| 78 |
+
|
| 79 |
+
scale = int(query.get('scale', [4])[0])
|
| 80 |
+
if scale not in [2, 4]:
|
| 81 |
+
scale = 4
|
| 82 |
+
|
| 83 |
+
from PIL import Image, ImageEnhance
|
| 84 |
+
input_image = Image.open(io.BytesIO(file_data))
|
| 85 |
+
|
| 86 |
+
if input_image.mode != "RGB":
|
| 87 |
+
input_image = input_image.convert("RGB")
|
| 88 |
+
|
| 89 |
+
new_size = (input_image.width * scale, input_image.height * scale)
|
| 90 |
+
upscaled = input_image.resize(new_size, Image.LANCZOS)
|
| 91 |
+
|
| 92 |
+
enhancer = ImageEnhance.Sharpness(upscaled)
|
| 93 |
+
sharpened = enhancer.enhance(1.3)
|
| 94 |
+
enhancer = ImageEnhance.Contrast(sharpened)
|
| 95 |
+
enhanced = enhancer.enhance(1.1)
|
| 96 |
+
|
| 97 |
+
file_id = str(uuid.uuid4())
|
| 98 |
+
output_path = OUTPUT_DIR / f"{file_id}_enhanced.png"
|
| 99 |
+
enhanced.save(output_path, "PNG")
|
| 100 |
+
|
| 101 |
+
if "/base64" in path:
|
| 102 |
+
import base64
|
| 103 |
+
buffer = io.BytesIO()
|
| 104 |
+
enhanced.save(buffer, format="PNG")
|
| 105 |
+
buffer.seek(0)
|
| 106 |
+
img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
| 107 |
+
|
| 108 |
+
self.send_json({
|
| 109 |
+
"success": True,
|
| 110 |
+
"image_base64": img_base64,
|
| 111 |
+
"original_size": {"width": input_image.width, "height": input_image.height},
|
| 112 |
+
"enhanced_size": {"width": enhanced.width, "height": enhanced.height},
|
| 113 |
+
"scale_factor": scale,
|
| 114 |
+
"note": "Preview mode - deploy to Hugging Face for AI enhancement"
|
| 115 |
+
})
|
| 116 |
+
else:
|
| 117 |
+
self.send_response(200)
|
| 118 |
+
self.send_header('Content-Type', 'image/png')
|
| 119 |
+
self.send_header('Content-Disposition', f'attachment; filename="enhanced_{filename.rsplit(".", 1)[0]}.png"')
|
| 120 |
+
self.end_headers()
|
| 121 |
+
with open(output_path, 'rb') as f:
|
| 122 |
+
self.wfile.write(f.read())
|
| 123 |
+
|
| 124 |
+
except Exception as e:
|
| 125 |
+
self.send_error(500, f"Error processing image: {str(e)}")
|
| 126 |
+
|
| 127 |
+
def serve_html(self):
|
| 128 |
+
html_path = Path("templates/index.html")
|
| 129 |
+
if html_path.exists():
|
| 130 |
+
self.send_response(200)
|
| 131 |
+
self.send_header('Content-Type', 'text/html')
|
| 132 |
+
self.send_header('Cache-Control', 'no-cache')
|
| 133 |
+
self.end_headers()
|
| 134 |
+
self.wfile.write(html_path.read_bytes())
|
| 135 |
+
else:
|
| 136 |
+
self.send_error(404, "Template not found")
|
| 137 |
+
|
| 138 |
+
def serve_swagger(self):
|
| 139 |
+
swagger_html = """<!DOCTYPE html>
|
| 140 |
+
<html>
|
| 141 |
+
<head>
|
| 142 |
+
<title>AI Image Enhancer - API Documentation</title>
|
| 143 |
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
| 144 |
+
<style>
|
| 145 |
+
body { margin: 0; padding: 0; }
|
| 146 |
+
.swagger-ui .topbar { display: none; }
|
| 147 |
+
</style>
|
| 148 |
+
</head>
|
| 149 |
+
<body>
|
| 150 |
+
<div id="swagger-ui"></div>
|
| 151 |
+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
| 152 |
+
<script>
|
| 153 |
+
window.onload = function() {
|
| 154 |
+
SwaggerUIBundle({
|
| 155 |
+
url: "/openapi.json",
|
| 156 |
+
dom_id: '#swagger-ui',
|
| 157 |
+
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
|
| 158 |
+
layout: "BaseLayout"
|
| 159 |
+
});
|
| 160 |
+
};
|
| 161 |
+
</script>
|
| 162 |
+
</body>
|
| 163 |
+
</html>"""
|
| 164 |
+
self.send_response(200)
|
| 165 |
+
self.send_header('Content-Type', 'text/html')
|
| 166 |
+
self.send_header('Cache-Control', 'no-cache')
|
| 167 |
+
self.end_headers()
|
| 168 |
+
self.wfile.write(swagger_html.encode())
|
| 169 |
+
|
| 170 |
+
def serve_openapi(self):
|
| 171 |
+
openapi_spec = {
|
| 172 |
+
"openapi": "3.1.0",
|
| 173 |
+
"info": {
|
| 174 |
+
"title": "AI Image Enhancer API",
|
| 175 |
+
"description": "AI-Powered Image Enhancement API using Real-ESRGAN deep learning model.\n\n**Features:**\n- Image upscaling up to 4x resolution\n- Quality enhancement and artifact removal\n- RESTful API with multiple output formats\n\n**Note:** This is a preview deployment. For full AI enhancement, deploy to Hugging Face Spaces.",
|
| 176 |
+
"version": "1.0.0",
|
| 177 |
+
"contact": {"name": "AI Image Enhancer"}
|
| 178 |
+
},
|
| 179 |
+
"servers": [{"url": "/", "description": "Current server"}],
|
| 180 |
+
"paths": {
|
| 181 |
+
"/enhance": {
|
| 182 |
+
"post": {
|
| 183 |
+
"summary": "Enhance an image",
|
| 184 |
+
"description": "Upload an image and receive an enhanced version with improved resolution and quality.",
|
| 185 |
+
"operationId": "enhance_image",
|
| 186 |
+
"parameters": [
|
| 187 |
+
{
|
| 188 |
+
"name": "scale",
|
| 189 |
+
"in": "query",
|
| 190 |
+
"description": "Upscale factor (2 or 4)",
|
| 191 |
+
"required": False,
|
| 192 |
+
"schema": {"type": "integer", "default": 4, "enum": [2, 4]}
|
| 193 |
+
}
|
| 194 |
+
],
|
| 195 |
+
"requestBody": {
|
| 196 |
+
"required": True,
|
| 197 |
+
"content": {
|
| 198 |
+
"multipart/form-data": {
|
| 199 |
+
"schema": {
|
| 200 |
+
"type": "object",
|
| 201 |
+
"required": ["file"],
|
| 202 |
+
"properties": {
|
| 203 |
+
"file": {
|
| 204 |
+
"type": "string",
|
| 205 |
+
"format": "binary",
|
| 206 |
+
"description": "Image file (PNG, JPG, JPEG, WebP, BMP)"
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
},
|
| 213 |
+
"responses": {
|
| 214 |
+
"200": {
|
| 215 |
+
"description": "Enhanced image",
|
| 216 |
+
"content": {"image/png": {"schema": {"type": "string", "format": "binary"}}}
|
| 217 |
+
},
|
| 218 |
+
"400": {"description": "Invalid file type"},
|
| 219 |
+
"500": {"description": "Processing error"}
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
},
|
| 223 |
+
"/enhance/base64": {
|
| 224 |
+
"post": {
|
| 225 |
+
"summary": "Enhance an image (Base64 response)",
|
| 226 |
+
"description": "Upload an image and receive an enhanced version as a base64-encoded string.",
|
| 227 |
+
"operationId": "enhance_image_base64",
|
| 228 |
+
"parameters": [
|
| 229 |
+
{
|
| 230 |
+
"name": "scale",
|
| 231 |
+
"in": "query",
|
| 232 |
+
"description": "Upscale factor (2 or 4)",
|
| 233 |
+
"required": False,
|
| 234 |
+
"schema": {"type": "integer", "default": 4, "enum": [2, 4]}
|
| 235 |
+
}
|
| 236 |
+
],
|
| 237 |
+
"requestBody": {
|
| 238 |
+
"required": True,
|
| 239 |
+
"content": {
|
| 240 |
+
"multipart/form-data": {
|
| 241 |
+
"schema": {
|
| 242 |
+
"type": "object",
|
| 243 |
+
"required": ["file"],
|
| 244 |
+
"properties": {
|
| 245 |
+
"file": {
|
| 246 |
+
"type": "string",
|
| 247 |
+
"format": "binary",
|
| 248 |
+
"description": "Image file"
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
},
|
| 255 |
+
"responses": {
|
| 256 |
+
"200": {
|
| 257 |
+
"description": "Enhanced image data",
|
| 258 |
+
"content": {
|
| 259 |
+
"application/json": {
|
| 260 |
+
"schema": {
|
| 261 |
+
"type": "object",
|
| 262 |
+
"properties": {
|
| 263 |
+
"success": {"type": "boolean"},
|
| 264 |
+
"image_base64": {"type": "string"},
|
| 265 |
+
"original_size": {"type": "object"},
|
| 266 |
+
"enhanced_size": {"type": "object"},
|
| 267 |
+
"scale_factor": {"type": "integer"}
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
}
|
| 275 |
+
},
|
| 276 |
+
"/health": {
|
| 277 |
+
"get": {
|
| 278 |
+
"summary": "Health check",
|
| 279 |
+
"description": "Check if the API is running",
|
| 280 |
+
"operationId": "health_check",
|
| 281 |
+
"responses": {
|
| 282 |
+
"200": {
|
| 283 |
+
"description": "API status",
|
| 284 |
+
"content": {"application/json": {"schema": {"type": "object"}}}
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
},
|
| 289 |
+
"/model-info": {
|
| 290 |
+
"get": {
|
| 291 |
+
"summary": "Model information",
|
| 292 |
+
"description": "Get information about the loaded AI model",
|
| 293 |
+
"operationId": "model_info",
|
| 294 |
+
"responses": {
|
| 295 |
+
"200": {
|
| 296 |
+
"description": "Model details",
|
| 297 |
+
"content": {"application/json": {"schema": {"type": "object"}}}
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
self.send_json(openapi_spec)
|
| 305 |
+
|
| 306 |
+
def serve_file(self, path):
|
| 307 |
+
file_path = Path("." + path)
|
| 308 |
+
if file_path.exists():
|
| 309 |
+
self.send_response(200)
|
| 310 |
+
self.send_header('Content-Type', 'image/png')
|
| 311 |
+
self.end_headers()
|
| 312 |
+
self.wfile.write(file_path.read_bytes())
|
| 313 |
+
else:
|
| 314 |
+
self.send_error(404, "File not found")
|
| 315 |
+
|
| 316 |
+
def send_json(self, data):
|
| 317 |
+
response = json.dumps(data, indent=2)
|
| 318 |
+
self.send_response(200)
|
| 319 |
+
self.send_header('Content-Type', 'application/json')
|
| 320 |
+
self.send_header('Cache-Control', 'no-cache')
|
| 321 |
+
self.end_headers()
|
| 322 |
+
self.wfile.write(response.encode())
|
| 323 |
+
|
| 324 |
+
def run_server(port=5000):
|
| 325 |
+
server_address = ('0.0.0.0', port)
|
| 326 |
+
httpd = HTTPServer(server_address, APIHandler)
|
| 327 |
+
print(f"AI Image Enhancer API running at http://0.0.0.0:{port}")
|
| 328 |
+
print(f"API Documentation: http://0.0.0.0:{port}/docs")
|
| 329 |
+
print(f"Frontend: http://0.0.0.0:{port}/")
|
| 330 |
+
print("\nNote: This is a preview mode using simple upscaling.")
|
| 331 |
+
print("For full AI enhancement, deploy to Hugging Face Spaces.")
|
| 332 |
+
httpd.serve_forever()
|
| 333 |
+
|
| 334 |
+
if __name__ == "__main__":
|
| 335 |
+
run_server()
|
enhancer.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import numpy as np
|
| 4 |
+
from PIL import Image
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
class ImageEnhancer:
|
| 8 |
+
"""
|
| 9 |
+
AI Image Enhancer using Real-ESRGAN model.
|
| 10 |
+
|
| 11 |
+
This class handles:
|
| 12 |
+
- Automatic model download from Hugging Face Hub
|
| 13 |
+
- Image preprocessing and postprocessing
|
| 14 |
+
- GPU/CPU inference
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def __init__(self, model_name: str = "RealESRGAN_x4plus"):
|
| 18 |
+
self.model_name = model_name
|
| 19 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 20 |
+
self.model = None
|
| 21 |
+
self._load_model()
|
| 22 |
+
|
| 23 |
+
def _load_model(self):
|
| 24 |
+
"""Download and load the Real-ESRGAN model."""
|
| 25 |
+
try:
|
| 26 |
+
from realesrgan import RealESRGANer
|
| 27 |
+
from basicsr.archs.rrdbnet_arch import RRDBNet
|
| 28 |
+
except ImportError:
|
| 29 |
+
print("Installing Real-ESRGAN dependencies...")
|
| 30 |
+
os.system("pip install realesrgan basicsr")
|
| 31 |
+
from realesrgan import RealESRGANer
|
| 32 |
+
from basicsr.archs.rrdbnet_arch import RRDBNet
|
| 33 |
+
|
| 34 |
+
model_path = Path("weights")
|
| 35 |
+
model_path.mkdir(exist_ok=True)
|
| 36 |
+
|
| 37 |
+
model_file = model_path / "RealESRGAN_x4plus.pth"
|
| 38 |
+
|
| 39 |
+
if not model_file.exists():
|
| 40 |
+
print("Downloading Real-ESRGAN x4plus model...")
|
| 41 |
+
import urllib.request
|
| 42 |
+
url = "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth"
|
| 43 |
+
urllib.request.urlretrieve(url, model_file)
|
| 44 |
+
print("Model downloaded successfully!")
|
| 45 |
+
|
| 46 |
+
model = RRDBNet(
|
| 47 |
+
num_in_ch=3,
|
| 48 |
+
num_out_ch=3,
|
| 49 |
+
num_feat=64,
|
| 50 |
+
num_block=23,
|
| 51 |
+
num_grow_ch=32,
|
| 52 |
+
scale=4
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
self.upsampler = RealESRGANer(
|
| 56 |
+
scale=4,
|
| 57 |
+
model_path=str(model_file),
|
| 58 |
+
model=model,
|
| 59 |
+
tile=0,
|
| 60 |
+
tile_pad=10,
|
| 61 |
+
pre_pad=0,
|
| 62 |
+
half=False if self.device.type == "cpu" else True,
|
| 63 |
+
device=self.device
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
print(f"Model loaded on {self.device}")
|
| 67 |
+
|
| 68 |
+
def enhance(self, image: Image.Image, scale: int = 4) -> Image.Image:
|
| 69 |
+
"""
|
| 70 |
+
Enhance an image using Real-ESRGAN.
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
image: PIL Image to enhance
|
| 74 |
+
scale: Upscaling factor (2 or 4)
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
Enhanced PIL Image
|
| 78 |
+
"""
|
| 79 |
+
img_array = np.array(image)
|
| 80 |
+
|
| 81 |
+
if len(img_array.shape) == 2:
|
| 82 |
+
img_array = np.stack([img_array] * 3, axis=-1)
|
| 83 |
+
elif img_array.shape[2] == 4:
|
| 84 |
+
img_array = img_array[:, :, :3]
|
| 85 |
+
|
| 86 |
+
img_bgr = img_array[:, :, ::-1]
|
| 87 |
+
|
| 88 |
+
output, _ = self.upsampler.enhance(img_bgr, outscale=scale)
|
| 89 |
+
|
| 90 |
+
output_rgb = output[:, :, ::-1]
|
| 91 |
+
enhanced_image = Image.fromarray(output_rgb)
|
| 92 |
+
|
| 93 |
+
return enhanced_image
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class FallbackEnhancer:
|
| 97 |
+
"""
|
| 98 |
+
Fallback enhancer using traditional image processing when AI model is unavailable.
|
| 99 |
+
Uses PIL's high-quality resampling for upscaling.
|
| 100 |
+
"""
|
| 101 |
+
|
| 102 |
+
def __init__(self):
|
| 103 |
+
print("Using fallback enhancer (no AI model available)")
|
| 104 |
+
|
| 105 |
+
def enhance(self, image: Image.Image, scale: int = 4) -> Image.Image:
|
| 106 |
+
"""
|
| 107 |
+
Enhance image using traditional upscaling with sharpening.
|
| 108 |
+
"""
|
| 109 |
+
from PIL import ImageEnhance, ImageFilter
|
| 110 |
+
|
| 111 |
+
new_size = (image.width * scale, image.height * scale)
|
| 112 |
+
upscaled = image.resize(new_size, Image.LANCZOS)
|
| 113 |
+
|
| 114 |
+
enhancer = ImageEnhance.Sharpness(upscaled)
|
| 115 |
+
sharpened = enhancer.enhance(1.3)
|
| 116 |
+
|
| 117 |
+
enhancer = ImageEnhance.Contrast(sharpened)
|
| 118 |
+
enhanced = enhancer.enhance(1.1)
|
| 119 |
+
|
| 120 |
+
return enhanced
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def get_enhancer():
|
| 124 |
+
"""
|
| 125 |
+
Factory function to get the best available enhancer.
|
| 126 |
+
Returns AI enhancer if available, otherwise falls back to traditional methods.
|
| 127 |
+
"""
|
| 128 |
+
try:
|
| 129 |
+
return ImageEnhancer()
|
| 130 |
+
except Exception as e:
|
| 131 |
+
print(f"Could not load AI model: {e}")
|
| 132 |
+
return FallbackEnhancer()
|
my_ssh_key.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBI7m5FxfcNAbUEc+X3FQ5BolyjdJA6xcr5oizuRXWmL runner@491f6838faa8
|
replit.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI Image Enhancer
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
An AI-powered image enhancement API using Real-ESRGAN for image super-resolution. The app includes:
|
| 5 |
+
- FastAPI backend with automatic Swagger API documentation
|
| 6 |
+
- Simple web frontend for testing image uploads
|
| 7 |
+
- Designed for deployment to Hugging Face Spaces
|
| 8 |
+
|
| 9 |
+
## Current State
|
| 10 |
+
- **Local Preview**: Running with simple upscaling (no AI model due to size constraints)
|
| 11 |
+
- **Full AI Mode**: Available when deployed to Hugging Face Spaces
|
| 12 |
+
|
| 13 |
+
## Project Structure
|
| 14 |
+
```
|
| 15 |
+
/
|
| 16 |
+
├── app.py # Full FastAPI app for Hugging Face deployment
|
| 17 |
+
├── app_local.py # Lightweight local preview server
|
| 18 |
+
├── enhancer.py # Real-ESRGAN model wrapper (for HF deployment)
|
| 19 |
+
├── templates/
|
| 20 |
+
│ └── index.html # Frontend interface
|
| 21 |
+
├── requirements.txt # Dependencies for Hugging Face Spaces
|
| 22 |
+
├── README.md # Hugging Face Spaces configuration
|
| 23 |
+
├── uploads/ # Temporary upload storage
|
| 24 |
+
└── outputs/ # Enhanced image outputs
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
## API Endpoints
|
| 28 |
+
- `GET /` - Web frontend
|
| 29 |
+
- `GET /docs` - Swagger API documentation
|
| 30 |
+
- `GET /health` - Health check
|
| 31 |
+
- `GET /model-info` - Model information
|
| 32 |
+
- `POST /enhance` - Enhance image (returns PNG file)
|
| 33 |
+
- `POST /enhance/base64` - Enhance image (returns base64 JSON)
|
| 34 |
+
|
| 35 |
+
## Deploying to Hugging Face Spaces
|
| 36 |
+
1. Create a new Space on Hugging Face
|
| 37 |
+
2. Select "Gradio" SDK (or "Docker" for more control)
|
| 38 |
+
3. Upload all files: `app.py`, `enhancer.py`, `templates/`, `requirements.txt`, `README.md`
|
| 39 |
+
4. The Space will auto-install dependencies and download the Real-ESRGAN model
|
| 40 |
+
|
| 41 |
+
## Recent Changes
|
| 42 |
+
- 2025-11-28: Initial creation of AI Image Enhancer API
|
| 43 |
+
- FastAPI backend with Swagger docs
|
| 44 |
+
- Real-ESRGAN integration for Hugging Face
|
| 45 |
+
- Simple frontend for testing
|
| 46 |
+
- Lightweight local preview mode
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.109.0
|
| 2 |
+
uvicorn[standard]==0.27.0
|
| 3 |
+
python-multipart==0.0.6
|
| 4 |
+
pillow==10.2.0
|
| 5 |
+
numpy==1.26.3
|
| 6 |
+
torch>=2.1.0
|
| 7 |
+
torchvision>=0.16.0
|
| 8 |
+
realesrgan==0.3.0
|
| 9 |
+
basicsr==1.4.2
|
| 10 |
+
gfpgan==1.3.8
|
| 11 |
+
pillow
|
templates/index.html
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>AI Image Enhancer</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
box-sizing: border-box;
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
| 16 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
| 17 |
+
min-height: 100vh;
|
| 18 |
+
color: #fff;
|
| 19 |
+
padding: 20px;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.container {
|
| 23 |
+
max-width: 900px;
|
| 24 |
+
margin: 0 auto;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
header {
|
| 28 |
+
text-align: center;
|
| 29 |
+
margin-bottom: 40px;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
h1 {
|
| 33 |
+
font-size: 2.5rem;
|
| 34 |
+
margin-bottom: 10px;
|
| 35 |
+
background: linear-gradient(90deg, #00d2ff, #3a7bd5);
|
| 36 |
+
-webkit-background-clip: text;
|
| 37 |
+
-webkit-text-fill-color: transparent;
|
| 38 |
+
background-clip: text;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.subtitle {
|
| 42 |
+
color: #888;
|
| 43 |
+
font-size: 1.1rem;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.api-link {
|
| 47 |
+
margin-top: 15px;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.api-link a {
|
| 51 |
+
color: #00d2ff;
|
| 52 |
+
text-decoration: none;
|
| 53 |
+
padding: 8px 16px;
|
| 54 |
+
border: 1px solid #00d2ff;
|
| 55 |
+
border-radius: 20px;
|
| 56 |
+
transition: all 0.3s;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.api-link a:hover {
|
| 60 |
+
background: #00d2ff;
|
| 61 |
+
color: #1a1a2e;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.upload-section {
|
| 65 |
+
background: rgba(255, 255, 255, 0.05);
|
| 66 |
+
border-radius: 16px;
|
| 67 |
+
padding: 30px;
|
| 68 |
+
margin-bottom: 30px;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.drop-zone {
|
| 72 |
+
border: 2px dashed #3a7bd5;
|
| 73 |
+
border-radius: 12px;
|
| 74 |
+
padding: 40px;
|
| 75 |
+
text-align: center;
|
| 76 |
+
cursor: pointer;
|
| 77 |
+
transition: all 0.3s;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.drop-zone:hover, .drop-zone.dragover {
|
| 81 |
+
border-color: #00d2ff;
|
| 82 |
+
background: rgba(0, 210, 255, 0.1);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.drop-zone p {
|
| 86 |
+
color: #888;
|
| 87 |
+
margin-top: 10px;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.drop-zone-icon {
|
| 91 |
+
font-size: 3rem;
|
| 92 |
+
color: #3a7bd5;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
#fileInput {
|
| 96 |
+
display: none;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.options {
|
| 100 |
+
display: flex;
|
| 101 |
+
gap: 20px;
|
| 102 |
+
margin-top: 20px;
|
| 103 |
+
flex-wrap: wrap;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.option-group {
|
| 107 |
+
flex: 1;
|
| 108 |
+
min-width: 200px;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.option-group label {
|
| 112 |
+
display: block;
|
| 113 |
+
margin-bottom: 8px;
|
| 114 |
+
color: #888;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
select {
|
| 118 |
+
width: 100%;
|
| 119 |
+
padding: 12px;
|
| 120 |
+
border-radius: 8px;
|
| 121 |
+
border: 1px solid #333;
|
| 122 |
+
background: #1a1a2e;
|
| 123 |
+
color: #fff;
|
| 124 |
+
font-size: 1rem;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.enhance-btn {
|
| 128 |
+
width: 100%;
|
| 129 |
+
padding: 15px;
|
| 130 |
+
margin-top: 20px;
|
| 131 |
+
background: linear-gradient(90deg, #00d2ff, #3a7bd5);
|
| 132 |
+
border: none;
|
| 133 |
+
border-radius: 8px;
|
| 134 |
+
color: #fff;
|
| 135 |
+
font-size: 1.1rem;
|
| 136 |
+
font-weight: 600;
|
| 137 |
+
cursor: pointer;
|
| 138 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.enhance-btn:hover:not(:disabled) {
|
| 142 |
+
transform: translateY(-2px);
|
| 143 |
+
box-shadow: 0 10px 30px rgba(0, 210, 255, 0.3);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.enhance-btn:disabled {
|
| 147 |
+
opacity: 0.5;
|
| 148 |
+
cursor: not-allowed;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.results-section {
|
| 152 |
+
display: none;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.results-section.show {
|
| 156 |
+
display: block;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.image-comparison {
|
| 160 |
+
display: grid;
|
| 161 |
+
grid-template-columns: 1fr 1fr;
|
| 162 |
+
gap: 20px;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
@media (max-width: 600px) {
|
| 166 |
+
.image-comparison {
|
| 167 |
+
grid-template-columns: 1fr;
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.image-box {
|
| 172 |
+
background: rgba(255, 255, 255, 0.05);
|
| 173 |
+
border-radius: 12px;
|
| 174 |
+
padding: 15px;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.image-box h3 {
|
| 178 |
+
margin-bottom: 10px;
|
| 179 |
+
color: #888;
|
| 180 |
+
font-size: 0.9rem;
|
| 181 |
+
text-transform: uppercase;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.image-box img {
|
| 185 |
+
width: 100%;
|
| 186 |
+
border-radius: 8px;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.download-btn {
|
| 190 |
+
display: inline-block;
|
| 191 |
+
margin-top: 15px;
|
| 192 |
+
padding: 10px 20px;
|
| 193 |
+
background: #3a7bd5;
|
| 194 |
+
color: #fff;
|
| 195 |
+
text-decoration: none;
|
| 196 |
+
border-radius: 6px;
|
| 197 |
+
transition: background 0.3s;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.download-btn:hover {
|
| 201 |
+
background: #00d2ff;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.loading {
|
| 205 |
+
display: none;
|
| 206 |
+
text-align: center;
|
| 207 |
+
padding: 40px;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.loading.show {
|
| 211 |
+
display: block;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.spinner {
|
| 215 |
+
width: 50px;
|
| 216 |
+
height: 50px;
|
| 217 |
+
border: 4px solid rgba(255, 255, 255, 0.1);
|
| 218 |
+
border-top-color: #00d2ff;
|
| 219 |
+
border-radius: 50%;
|
| 220 |
+
animation: spin 1s linear infinite;
|
| 221 |
+
margin: 0 auto 20px;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
@keyframes spin {
|
| 225 |
+
to { transform: rotate(360deg); }
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.error {
|
| 229 |
+
background: rgba(255, 50, 50, 0.2);
|
| 230 |
+
border: 1px solid #ff3232;
|
| 231 |
+
padding: 15px;
|
| 232 |
+
border-radius: 8px;
|
| 233 |
+
margin-top: 20px;
|
| 234 |
+
display: none;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.error.show {
|
| 238 |
+
display: block;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.info-section {
|
| 242 |
+
background: rgba(255, 255, 255, 0.05);
|
| 243 |
+
border-radius: 16px;
|
| 244 |
+
padding: 30px;
|
| 245 |
+
margin-top: 30px;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.info-section h2 {
|
| 249 |
+
margin-bottom: 20px;
|
| 250 |
+
color: #00d2ff;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.info-grid {
|
| 254 |
+
display: grid;
|
| 255 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 256 |
+
gap: 20px;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.info-item {
|
| 260 |
+
background: rgba(255, 255, 255, 0.05);
|
| 261 |
+
padding: 15px;
|
| 262 |
+
border-radius: 8px;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.info-item h4 {
|
| 266 |
+
color: #3a7bd5;
|
| 267 |
+
margin-bottom: 5px;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.info-item p {
|
| 271 |
+
color: #888;
|
| 272 |
+
font-size: 0.9rem;
|
| 273 |
+
}
|
| 274 |
+
</style>
|
| 275 |
+
</head>
|
| 276 |
+
<body>
|
| 277 |
+
<div class="container">
|
| 278 |
+
<header>
|
| 279 |
+
<h1>AI Image Enhancer</h1>
|
| 280 |
+
<p class="subtitle">Enhance your images using Real-ESRGAN deep learning model</p>
|
| 281 |
+
<div class="api-link">
|
| 282 |
+
<a href="/docs" target="_blank">View API Documentation</a>
|
| 283 |
+
</div>
|
| 284 |
+
</header>
|
| 285 |
+
|
| 286 |
+
<section class="upload-section">
|
| 287 |
+
<div class="drop-zone" id="dropZone">
|
| 288 |
+
<div class="drop-zone-icon">📷</div>
|
| 289 |
+
<p>Drag & drop an image here or click to select</p>
|
| 290 |
+
<p><small>Supports: PNG, JPG, JPEG, WebP, BMP</small></p>
|
| 291 |
+
</div>
|
| 292 |
+
<input type="file" id="fileInput" accept="image/png,image/jpeg,image/jpg,image/webp,image/bmp">
|
| 293 |
+
|
| 294 |
+
<div class="options">
|
| 295 |
+
<div class="option-group">
|
| 296 |
+
<label for="scale">Upscale Factor</label>
|
| 297 |
+
<select id="scale">
|
| 298 |
+
<option value="2">2x - Double resolution</option>
|
| 299 |
+
<option value="4" selected>4x - Quadruple resolution</option>
|
| 300 |
+
</select>
|
| 301 |
+
</div>
|
| 302 |
+
</div>
|
| 303 |
+
|
| 304 |
+
<button class="enhance-btn" id="enhanceBtn" disabled>Enhance Image</button>
|
| 305 |
+
|
| 306 |
+
<div class="error" id="error"></div>
|
| 307 |
+
</section>
|
| 308 |
+
|
| 309 |
+
<div class="loading" id="loading">
|
| 310 |
+
<div class="spinner"></div>
|
| 311 |
+
<p>Enhancing your image with AI magic...</p>
|
| 312 |
+
<p><small>This may take a moment for larger images</small></p>
|
| 313 |
+
</div>
|
| 314 |
+
|
| 315 |
+
<section class="results-section" id="results">
|
| 316 |
+
<div class="image-comparison">
|
| 317 |
+
<div class="image-box">
|
| 318 |
+
<h3>Original</h3>
|
| 319 |
+
<img id="originalImg" src="" alt="Original image">
|
| 320 |
+
</div>
|
| 321 |
+
<div class="image-box">
|
| 322 |
+
<h3>Enhanced</h3>
|
| 323 |
+
<img id="enhancedImg" src="" alt="Enhanced image">
|
| 324 |
+
<a href="#" class="download-btn" id="downloadBtn" download="enhanced_image.png">Download Enhanced Image</a>
|
| 325 |
+
</div>
|
| 326 |
+
</div>
|
| 327 |
+
</section>
|
| 328 |
+
|
| 329 |
+
<section class="info-section">
|
| 330 |
+
<h2>About the Model</h2>
|
| 331 |
+
<div class="info-grid">
|
| 332 |
+
<div class="info-item">
|
| 333 |
+
<h4>Real-ESRGAN x4plus</h4>
|
| 334 |
+
<p>State-of-the-art image super-resolution model</p>
|
| 335 |
+
</div>
|
| 336 |
+
<div class="info-item">
|
| 337 |
+
<h4>Deep Learning</h4>
|
| 338 |
+
<p>Uses RRDB architecture for high-quality results</p>
|
| 339 |
+
</div>
|
| 340 |
+
<div class="info-item">
|
| 341 |
+
<h4>Multi-purpose</h4>
|
| 342 |
+
<p>Works on photos, artwork, and various image types</p>
|
| 343 |
+
</div>
|
| 344 |
+
<div class="info-item">
|
| 345 |
+
<h4>API Access</h4>
|
| 346 |
+
<p>RESTful API with Swagger documentation at /docs</p>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
</section>
|
| 350 |
+
</div>
|
| 351 |
+
|
| 352 |
+
<script>
|
| 353 |
+
const dropZone = document.getElementById('dropZone');
|
| 354 |
+
const fileInput = document.getElementById('fileInput');
|
| 355 |
+
const enhanceBtn = document.getElementById('enhanceBtn');
|
| 356 |
+
const loading = document.getElementById('loading');
|
| 357 |
+
const results = document.getElementById('results');
|
| 358 |
+
const error = document.getElementById('error');
|
| 359 |
+
const originalImg = document.getElementById('originalImg');
|
| 360 |
+
const enhancedImg = document.getElementById('enhancedImg');
|
| 361 |
+
const downloadBtn = document.getElementById('downloadBtn');
|
| 362 |
+
const scaleSelect = document.getElementById('scale');
|
| 363 |
+
|
| 364 |
+
let selectedFile = null;
|
| 365 |
+
|
| 366 |
+
dropZone.addEventListener('click', () => fileInput.click());
|
| 367 |
+
|
| 368 |
+
dropZone.addEventListener('dragover', (e) => {
|
| 369 |
+
e.preventDefault();
|
| 370 |
+
dropZone.classList.add('dragover');
|
| 371 |
+
});
|
| 372 |
+
|
| 373 |
+
dropZone.addEventListener('dragleave', () => {
|
| 374 |
+
dropZone.classList.remove('dragover');
|
| 375 |
+
});
|
| 376 |
+
|
| 377 |
+
dropZone.addEventListener('drop', (e) => {
|
| 378 |
+
e.preventDefault();
|
| 379 |
+
dropZone.classList.remove('dragover');
|
| 380 |
+
const files = e.dataTransfer.files;
|
| 381 |
+
if (files.length > 0) {
|
| 382 |
+
handleFile(files[0]);
|
| 383 |
+
}
|
| 384 |
+
});
|
| 385 |
+
|
| 386 |
+
fileInput.addEventListener('change', (e) => {
|
| 387 |
+
if (e.target.files.length > 0) {
|
| 388 |
+
handleFile(e.target.files[0]);
|
| 389 |
+
}
|
| 390 |
+
});
|
| 391 |
+
|
| 392 |
+
function handleFile(file) {
|
| 393 |
+
if (!file.type.match(/^image\/(png|jpeg|jpg|webp|bmp)$/)) {
|
| 394 |
+
showError('Please select a valid image file (PNG, JPG, JPEG, WebP, or BMP)');
|
| 395 |
+
return;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
selectedFile = file;
|
| 399 |
+
enhanceBtn.disabled = false;
|
| 400 |
+
dropZone.innerHTML = `
|
| 401 |
+
<div class="drop-zone-icon">✅</div>
|
| 402 |
+
<p><strong>${file.name}</strong></p>
|
| 403 |
+
<p><small>${(file.size / 1024 / 1024).toFixed(2)} MB</small></p>
|
| 404 |
+
`;
|
| 405 |
+
|
| 406 |
+
const reader = new FileReader();
|
| 407 |
+
reader.onload = (e) => {
|
| 408 |
+
originalImg.src = e.target.result;
|
| 409 |
+
};
|
| 410 |
+
reader.readAsDataURL(file);
|
| 411 |
+
|
| 412 |
+
hideError();
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
enhanceBtn.addEventListener('click', async () => {
|
| 416 |
+
if (!selectedFile) return;
|
| 417 |
+
|
| 418 |
+
const scale = scaleSelect.value;
|
| 419 |
+
const formData = new FormData();
|
| 420 |
+
formData.append('file', selectedFile);
|
| 421 |
+
|
| 422 |
+
loading.classList.add('show');
|
| 423 |
+
results.classList.remove('show');
|
| 424 |
+
enhanceBtn.disabled = true;
|
| 425 |
+
hideError();
|
| 426 |
+
|
| 427 |
+
try {
|
| 428 |
+
const response = await fetch(`/enhance?scale=${scale}`, {
|
| 429 |
+
method: 'POST',
|
| 430 |
+
body: formData
|
| 431 |
+
});
|
| 432 |
+
|
| 433 |
+
if (!response.ok) {
|
| 434 |
+
const errorData = await response.json();
|
| 435 |
+
throw new Error(errorData.detail || 'Enhancement failed');
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
const blob = await response.blob();
|
| 439 |
+
const imageUrl = URL.createObjectURL(blob);
|
| 440 |
+
|
| 441 |
+
enhancedImg.src = imageUrl;
|
| 442 |
+
downloadBtn.href = imageUrl;
|
| 443 |
+
|
| 444 |
+
loading.classList.remove('show');
|
| 445 |
+
results.classList.add('show');
|
| 446 |
+
|
| 447 |
+
} catch (err) {
|
| 448 |
+
showError(err.message);
|
| 449 |
+
loading.classList.remove('show');
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
enhanceBtn.disabled = false;
|
| 453 |
+
});
|
| 454 |
+
|
| 455 |
+
function showError(message) {
|
| 456 |
+
error.textContent = message;
|
| 457 |
+
error.classList.add('show');
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
function hideError() {
|
| 461 |
+
error.classList.remove('show');
|
| 462 |
+
}
|
| 463 |
+
</script>
|
| 464 |
+
</body>
|
| 465 |
+
</html>
|