Spaces:
Sleeping
Sleeping
Deploy Real-CUGAN control plane with bundled weights
Browse files- .gitignore +6 -0
- Dockerfile +29 -0
- README.md +85 -6
- app/__init__.py +1 -0
- app/config.py +202 -0
- app/db.py +313 -0
- app/main.py +491 -0
- app/runtime.py +366 -0
- app/security.py +50 -0
- app/static/dashboard.js +48 -0
- app/static/styles.css +663 -0
- app/templates/base.html +19 -0
- app/templates/dashboard.html +357 -0
- app/templates/login.html +85 -0
- app/vendor/__init__.py +1 -0
- app/vendor/upcunet_v3.py +1334 -0
- requirements.txt +8 -0
- weights/up2x-latest-conservative.pth +3 -0
- weights/up2x-latest-denoise1x.pth +3 -0
- weights/up2x-latest-denoise2x.pth +3 -0
- weights/up2x-latest-denoise3x.pth +3 -0
- weights/up2x-latest-no-denoise.pth +3 -0
- weights/up3x-latest-conservative.pth +3 -0
- weights/up3x-latest-denoise3x.pth +3 -0
- weights/up3x-latest-no-denoise.pth +3 -0
- weights/up4x-latest-conservative.pth +3 -0
- weights/up4x-latest-denoise3x.pth +3 -0
- weights/up4x-latest-no-denoise.pth +3 -0
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
.venv/
|
| 4 |
+
app/__pycache__/
|
| 5 |
+
app/vendor/__pycache__/
|
| 6 |
+
data/*.db
|
Dockerfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 4 |
+
ENV PIP_NO_CACHE_DIR=1
|
| 5 |
+
ENV PYTHONUNBUFFERED=1
|
| 6 |
+
|
| 7 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 8 |
+
libgomp1 \
|
| 9 |
+
curl \
|
| 10 |
+
ca-certificates \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
WORKDIR /app
|
| 14 |
+
|
| 15 |
+
COPY requirements.txt /app/requirements.txt
|
| 16 |
+
|
| 17 |
+
RUN pip install --no-cache-dir --upgrade pip \
|
| 18 |
+
&& pip install --no-cache-dir torch==2.5.1 --index-url https://download.pytorch.org/whl/cpu \
|
| 19 |
+
&& pip install --no-cache-dir -r /app/requirements.txt
|
| 20 |
+
|
| 21 |
+
COPY app /app/app
|
| 22 |
+
COPY weights /app/weights
|
| 23 |
+
COPY README.md /app/README.md
|
| 24 |
+
|
| 25 |
+
RUN mkdir -p /app/data /app/app/vendor
|
| 26 |
+
|
| 27 |
+
EXPOSE 7860
|
| 28 |
+
|
| 29 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,10 +1,89 @@
|
|
| 1 |
---
|
| 2 |
-
title: Real
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Real-CUGAN Control Plane
|
| 3 |
+
emoji: 🖼️
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# Real-CUGAN Control Plane
|
| 11 |
+
|
| 12 |
+
CPU-first Hugging Face Space for anime image upscaling with:
|
| 13 |
+
|
| 14 |
+
- API key protected `/api/v1/upscale`
|
| 15 |
+
- Admin login, key management, runtime controls
|
| 16 |
+
- Job history and lightweight dashboard
|
| 17 |
+
- Real-CUGAN lazy loading on CPU
|
| 18 |
+
|
| 19 |
+
## Required setup
|
| 20 |
+
|
| 21 |
+
1. Put one or more Real-CUGAN weight files into `weights/`.
|
| 22 |
+
2. Set `ADMIN_PASSWORD` as a Hugging Face Space secret.
|
| 23 |
+
3. Optional: set `SESSION_SECRET`, `BOOTSTRAP_API_KEY`, `REAL_CUGAN_WEIGHT_BASE_URL`.
|
| 24 |
+
|
| 25 |
+
Recommended Hugging Face Space secrets:
|
| 26 |
+
|
| 27 |
+
- `ADMIN_PASSWORD`: required unless you want to initialize the admin account in the browser
|
| 28 |
+
- `SESSION_SECRET`: recommended for stable admin sessions across restarts
|
| 29 |
+
- `BOOTSTRAP_API_KEY`: optional first API key created at boot
|
| 30 |
+
- `BOOTSTRAP_API_KEY_NAME`: optional label for the bootstrap key
|
| 31 |
+
- `REAL_CUGAN_WEIGHT_BASE_URL`: optional base URL if you host weights remotely
|
| 32 |
+
- `SESSION_HTTPS_ONLY=true`: recommended once the Space is running behind HTTPS
|
| 33 |
+
|
| 34 |
+
Recommended 2x weights:
|
| 35 |
+
|
| 36 |
+
- `up2x-latest-no-denoise.pth`
|
| 37 |
+
- `up2x-latest-denoise1x.pth`
|
| 38 |
+
- `up2x-latest-denoise2x.pth`
|
| 39 |
+
- `up2x-latest-denoise3x.pth`
|
| 40 |
+
- `up2x-latest-conservative.pth`
|
| 41 |
+
|
| 42 |
+
Optional 3x / 4x weights:
|
| 43 |
+
|
| 44 |
+
- `up3x-latest-no-denoise.pth`
|
| 45 |
+
- `up3x-latest-denoise3x.pth`
|
| 46 |
+
- `up3x-latest-conservative.pth`
|
| 47 |
+
- `up4x-latest-no-denoise.pth`
|
| 48 |
+
- `up4x-latest-denoise3x.pth`
|
| 49 |
+
- `up4x-latest-conservative.pth`
|
| 50 |
+
|
| 51 |
+
## API
|
| 52 |
+
|
| 53 |
+
Use either `X-API-Key: <key>` or `Authorization: Bearer <key>`.
|
| 54 |
+
|
| 55 |
+
`POST /api/v1/upscale`
|
| 56 |
+
|
| 57 |
+
Multipart form fields:
|
| 58 |
+
|
| 59 |
+
- `file`: source image
|
| 60 |
+
- `scale`: `2`, `3`, `4`
|
| 61 |
+
- `variant`: `no-denoise`, `denoise1x`, `denoise2x`, `denoise3x`, `conservative`
|
| 62 |
+
- `alpha`: `0.75` to `1.3`
|
| 63 |
+
- `tile_mode`: optional override
|
| 64 |
+
- `cache_mode`: optional override
|
| 65 |
+
- `response_format`: `png`, `jpeg`, `webp`
|
| 66 |
+
|
| 67 |
+
The admin panel is at `/admin`.
|
| 68 |
+
|
| 69 |
+
## Local run
|
| 70 |
+
|
| 71 |
+
```bash
|
| 72 |
+
cd /home/huanx/code/real-cugan-hf-api
|
| 73 |
+
python -m venv .venv
|
| 74 |
+
source .venv/bin/activate
|
| 75 |
+
pip install --upgrade pip
|
| 76 |
+
pip install torch==2.5.1 --index-url https://download.pytorch.org/whl/cpu
|
| 77 |
+
pip install -r requirements.txt
|
| 78 |
+
export ADMIN_PASSWORD='change-me-now'
|
| 79 |
+
uvicorn app.main:app --host 0.0.0.0 --port 7860
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
Open `http://127.0.0.1:7860/admin`.
|
| 83 |
+
|
| 84 |
+
## Notes
|
| 85 |
+
|
| 86 |
+
- The app stores only metadata in SQLite. Uploaded images are processed in memory and are not persisted by default.
|
| 87 |
+
- The runtime loads `upcunet_v3.py` from the pinned Real-CUGAN upstream commit on first use and verifies its checksum.
|
| 88 |
+
- API requests are serialized. One active upscale runs at a time by design.
|
| 89 |
+
- Without CPU PyTorch or missing weights, the API responds with a clear `503` describing the runtime issue.
|
app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Real-CUGAN control plane package."""
|
app/config.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import os
|
| 6 |
+
import secrets
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def _bool_env(name: str, default: bool) -> bool:
|
| 10 |
+
value = os.getenv(name)
|
| 11 |
+
if value is None:
|
| 12 |
+
return default
|
| 13 |
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _int_env(name: str, default: int) -> int:
|
| 17 |
+
value = os.getenv(name)
|
| 18 |
+
if value is None:
|
| 19 |
+
return default
|
| 20 |
+
try:
|
| 21 |
+
return int(value)
|
| 22 |
+
except ValueError:
|
| 23 |
+
return default
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
| 27 |
+
DATA_DIR = PROJECT_ROOT / "data"
|
| 28 |
+
WEIGHTS_DIR = PROJECT_ROOT / "weights"
|
| 29 |
+
VENDOR_DIR = PROJECT_ROOT / "app" / "vendor"
|
| 30 |
+
|
| 31 |
+
REAL_CUGAN_COMMIT = "2799af78ef105b414cc4b796c67c8511acdcdf6f"
|
| 32 |
+
REAL_CUGAN_SHA256 = "ef6c4e433bcac37b75ffba0a4044987ddd3ecfe7765a74a3c93887954e45562b"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
MODEL_CATALOG = {
|
| 36 |
+
2: {
|
| 37 |
+
"no-denoise": [
|
| 38 |
+
"up2x-latest-no-denoise.pth",
|
| 39 |
+
"up2x-no-denoise.pth",
|
| 40 |
+
],
|
| 41 |
+
"denoise1x": [
|
| 42 |
+
"up2x-latest-denoise1x.pth",
|
| 43 |
+
],
|
| 44 |
+
"denoise2x": [
|
| 45 |
+
"up2x-latest-denoise2x.pth",
|
| 46 |
+
],
|
| 47 |
+
"denoise3x": [
|
| 48 |
+
"up2x-latest-denoise3x.pth",
|
| 49 |
+
],
|
| 50 |
+
"conservative": [
|
| 51 |
+
"up2x-latest-conservative.pth",
|
| 52 |
+
],
|
| 53 |
+
},
|
| 54 |
+
3: {
|
| 55 |
+
"no-denoise": [
|
| 56 |
+
"up3x-latest-no-denoise.pth",
|
| 57 |
+
"up3x-no-denoise.pth",
|
| 58 |
+
],
|
| 59 |
+
"denoise3x": [
|
| 60 |
+
"up3x-latest-denoise3x.pth",
|
| 61 |
+
],
|
| 62 |
+
"conservative": [
|
| 63 |
+
"up3x-latest-conservative.pth",
|
| 64 |
+
],
|
| 65 |
+
},
|
| 66 |
+
4: {
|
| 67 |
+
"no-denoise": [
|
| 68 |
+
"up4x-latest-no-denoise.pth",
|
| 69 |
+
"up4x-no-denoise.pth",
|
| 70 |
+
],
|
| 71 |
+
"denoise3x": [
|
| 72 |
+
"up4x-latest-denoise3x.pth",
|
| 73 |
+
],
|
| 74 |
+
"conservative": [
|
| 75 |
+
"up4x-latest-conservative.pth",
|
| 76 |
+
],
|
| 77 |
+
},
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
RUNTIME_FIELD_SPECS = [
|
| 82 |
+
{
|
| 83 |
+
"name": "default_scale",
|
| 84 |
+
"label": "Default scale",
|
| 85 |
+
"type": "select",
|
| 86 |
+
"options": ["2", "3", "4"],
|
| 87 |
+
"help": "Fallback scale when the API request omits it.",
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"name": "default_variant",
|
| 91 |
+
"label": "Default variant",
|
| 92 |
+
"type": "select",
|
| 93 |
+
"options": [
|
| 94 |
+
"no-denoise",
|
| 95 |
+
"denoise1x",
|
| 96 |
+
"denoise2x",
|
| 97 |
+
"denoise3x",
|
| 98 |
+
"conservative",
|
| 99 |
+
],
|
| 100 |
+
"help": "Default Real-CUGAN weight profile.",
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"name": "default_alpha",
|
| 104 |
+
"label": "Alpha",
|
| 105 |
+
"type": "float",
|
| 106 |
+
"help": "Enhancement strength. Official safe range is 0.75 to 1.3.",
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"name": "default_tile_mode",
|
| 110 |
+
"label": "Tile mode",
|
| 111 |
+
"type": "int",
|
| 112 |
+
"help": "0 prefers whole-image inference. Higher values trade speed for memory safety.",
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"name": "default_cache_mode",
|
| 116 |
+
"label": "Cache mode",
|
| 117 |
+
"type": "select",
|
| 118 |
+
"options": ["0", "1"],
|
| 119 |
+
"help": "1 is slower but safer under memory pressure on CPU.",
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"name": "max_input_long_edge",
|
| 123 |
+
"label": "Max input long edge",
|
| 124 |
+
"type": "int",
|
| 125 |
+
"help": "Requests above this are rejected before inference.",
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"name": "max_input_pixels",
|
| 129 |
+
"label": "Max input pixels",
|
| 130 |
+
"type": "int",
|
| 131 |
+
"help": "Absolute input pixel cap for CPU safety.",
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"name": "jpeg_quality",
|
| 135 |
+
"label": "JPEG quality",
|
| 136 |
+
"type": "int",
|
| 137 |
+
"help": "Applied when the response format is jpeg.",
|
| 138 |
+
},
|
| 139 |
+
]
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
DEFAULT_RUNTIME_SETTINGS = {
|
| 143 |
+
"default_scale": "2",
|
| 144 |
+
"default_variant": "no-denoise",
|
| 145 |
+
"default_alpha": "1.0",
|
| 146 |
+
"default_tile_mode": "3",
|
| 147 |
+
"default_cache_mode": "1",
|
| 148 |
+
"max_input_long_edge": "1536",
|
| 149 |
+
"max_input_pixels": "2400000",
|
| 150 |
+
"jpeg_quality": "95",
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@dataclass(slots=True)
|
| 155 |
+
class EnvSettings:
|
| 156 |
+
app_name: str
|
| 157 |
+
host: str
|
| 158 |
+
port: int
|
| 159 |
+
database_path: Path
|
| 160 |
+
data_dir: Path
|
| 161 |
+
weights_dir: Path
|
| 162 |
+
vendor_dir: Path
|
| 163 |
+
session_secret: str
|
| 164 |
+
session_https_only: bool
|
| 165 |
+
admin_username: str
|
| 166 |
+
admin_password: str
|
| 167 |
+
bootstrap_api_key: str
|
| 168 |
+
bootstrap_api_key_name: str
|
| 169 |
+
real_cugan_commit: str
|
| 170 |
+
real_cugan_sha256: str
|
| 171 |
+
real_cugan_upstream_url: str
|
| 172 |
+
real_cugan_weight_base_url: str
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def load_env_settings() -> EnvSettings:
|
| 176 |
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
| 177 |
+
WEIGHTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 178 |
+
VENDOR_DIR.mkdir(parents=True, exist_ok=True)
|
| 179 |
+
session_secret = os.getenv("SESSION_SECRET") or secrets.token_urlsafe(32)
|
| 180 |
+
commit = os.getenv("REAL_CUGAN_COMMIT", REAL_CUGAN_COMMIT)
|
| 181 |
+
return EnvSettings(
|
| 182 |
+
app_name=os.getenv("APP_NAME", "Real-CUGAN Control Plane"),
|
| 183 |
+
host=os.getenv("HOST", "0.0.0.0"),
|
| 184 |
+
port=_int_env("PORT", 7860),
|
| 185 |
+
database_path=Path(os.getenv("DATABASE_PATH", DATA_DIR / "app.db")),
|
| 186 |
+
data_dir=DATA_DIR,
|
| 187 |
+
weights_dir=WEIGHTS_DIR,
|
| 188 |
+
vendor_dir=VENDOR_DIR,
|
| 189 |
+
session_secret=session_secret,
|
| 190 |
+
session_https_only=_bool_env("SESSION_HTTPS_ONLY", False),
|
| 191 |
+
admin_username=os.getenv("ADMIN_USERNAME", "admin"),
|
| 192 |
+
admin_password=os.getenv("ADMIN_PASSWORD", ""),
|
| 193 |
+
bootstrap_api_key=os.getenv("BOOTSTRAP_API_KEY", ""),
|
| 194 |
+
bootstrap_api_key_name=os.getenv("BOOTSTRAP_API_KEY_NAME", "primary"),
|
| 195 |
+
real_cugan_commit=commit,
|
| 196 |
+
real_cugan_sha256=os.getenv("REAL_CUGAN_SHA256", REAL_CUGAN_SHA256),
|
| 197 |
+
real_cugan_upstream_url=os.getenv(
|
| 198 |
+
"REAL_CUGAN_UPSTREAM_URL",
|
| 199 |
+
f"https://raw.githubusercontent.com/bilibili/ailab/{commit}/Real-CUGAN/upcunet_v3.py",
|
| 200 |
+
),
|
| 201 |
+
real_cugan_weight_base_url=os.getenv("REAL_CUGAN_WEIGHT_BASE_URL", "").rstrip("/"),
|
| 202 |
+
)
|
app/db.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import sqlite3
|
| 4 |
+
import threading
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def utcnow_iso() -> str:
|
| 10 |
+
from datetime import datetime, timezone
|
| 11 |
+
|
| 12 |
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class Database:
|
| 16 |
+
def __init__(self, path: Path) -> None:
|
| 17 |
+
self.path = path
|
| 18 |
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
| 19 |
+
self._lock = threading.RLock()
|
| 20 |
+
self._conn = sqlite3.connect(self.path, check_same_thread=False)
|
| 21 |
+
self._conn.row_factory = sqlite3.Row
|
| 22 |
+
|
| 23 |
+
def close(self) -> None:
|
| 24 |
+
self._conn.close()
|
| 25 |
+
|
| 26 |
+
def initialize(self) -> None:
|
| 27 |
+
with self._lock:
|
| 28 |
+
self._conn.executescript(
|
| 29 |
+
"""
|
| 30 |
+
PRAGMA journal_mode=WAL;
|
| 31 |
+
|
| 32 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 33 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 34 |
+
username TEXT NOT NULL UNIQUE,
|
| 35 |
+
password_hash TEXT NOT NULL,
|
| 36 |
+
is_admin INTEGER NOT NULL DEFAULT 1,
|
| 37 |
+
created_at TEXT NOT NULL,
|
| 38 |
+
updated_at TEXT NOT NULL
|
| 39 |
+
);
|
| 40 |
+
|
| 41 |
+
CREATE TABLE IF NOT EXISTS api_keys (
|
| 42 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 43 |
+
name TEXT NOT NULL,
|
| 44 |
+
key_prefix TEXT NOT NULL UNIQUE,
|
| 45 |
+
secret_hash TEXT NOT NULL,
|
| 46 |
+
enabled INTEGER NOT NULL DEFAULT 1,
|
| 47 |
+
created_at TEXT NOT NULL,
|
| 48 |
+
last_used_at TEXT
|
| 49 |
+
);
|
| 50 |
+
|
| 51 |
+
CREATE TABLE IF NOT EXISTS app_settings (
|
| 52 |
+
name TEXT PRIMARY KEY,
|
| 53 |
+
value TEXT NOT NULL,
|
| 54 |
+
updated_at TEXT NOT NULL
|
| 55 |
+
);
|
| 56 |
+
|
| 57 |
+
CREATE TABLE IF NOT EXISTS jobs (
|
| 58 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 59 |
+
status TEXT NOT NULL,
|
| 60 |
+
source_filename TEXT,
|
| 61 |
+
input_sha256 TEXT,
|
| 62 |
+
input_width INTEGER,
|
| 63 |
+
input_height INTEGER,
|
| 64 |
+
output_width INTEGER,
|
| 65 |
+
output_height INTEGER,
|
| 66 |
+
scale INTEGER,
|
| 67 |
+
model_variant TEXT,
|
| 68 |
+
alpha REAL,
|
| 69 |
+
tile_mode_requested INTEGER,
|
| 70 |
+
tile_mode_used INTEGER,
|
| 71 |
+
cache_mode INTEGER,
|
| 72 |
+
response_format TEXT,
|
| 73 |
+
duration_ms INTEGER,
|
| 74 |
+
error_message TEXT,
|
| 75 |
+
created_at TEXT NOT NULL,
|
| 76 |
+
started_at TEXT,
|
| 77 |
+
finished_at TEXT
|
| 78 |
+
);
|
| 79 |
+
"""
|
| 80 |
+
)
|
| 81 |
+
self._conn.commit()
|
| 82 |
+
|
| 83 |
+
def fetch_one(self, query: str, params: tuple[Any, ...] = ()) -> sqlite3.Row | None:
|
| 84 |
+
with self._lock:
|
| 85 |
+
return self._conn.execute(query, params).fetchone()
|
| 86 |
+
|
| 87 |
+
def fetch_all(self, query: str, params: tuple[Any, ...] = ()) -> list[sqlite3.Row]:
|
| 88 |
+
with self._lock:
|
| 89 |
+
return self._conn.execute(query, params).fetchall()
|
| 90 |
+
|
| 91 |
+
def execute(self, query: str, params: tuple[Any, ...] = ()) -> sqlite3.Cursor:
|
| 92 |
+
with self._lock:
|
| 93 |
+
cursor = self._conn.execute(query, params)
|
| 94 |
+
self._conn.commit()
|
| 95 |
+
return cursor
|
| 96 |
+
|
| 97 |
+
def ensure_admin_user(self, username: str, password_hash: str) -> None:
|
| 98 |
+
existing = self.fetch_one("SELECT id FROM users WHERE username = ?", (username,))
|
| 99 |
+
now = utcnow_iso()
|
| 100 |
+
if existing:
|
| 101 |
+
return
|
| 102 |
+
self.execute(
|
| 103 |
+
"""
|
| 104 |
+
INSERT INTO users (username, password_hash, is_admin, created_at, updated_at)
|
| 105 |
+
VALUES (?, ?, 1, ?, ?)
|
| 106 |
+
""",
|
| 107 |
+
(username, password_hash, now, now),
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
def update_admin_password(self, username: str, password_hash: str) -> None:
|
| 111 |
+
self.execute(
|
| 112 |
+
"UPDATE users SET password_hash = ?, updated_at = ? WHERE username = ?",
|
| 113 |
+
(password_hash, utcnow_iso(), username),
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
def get_user_by_username(self, username: str) -> sqlite3.Row | None:
|
| 117 |
+
return self.fetch_one("SELECT * FROM users WHERE username = ?", (username,))
|
| 118 |
+
|
| 119 |
+
def get_user_by_id(self, user_id: int) -> sqlite3.Row | None:
|
| 120 |
+
return self.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 121 |
+
|
| 122 |
+
def create_api_key(self, name: str, prefix: str, secret_hash: str) -> int:
|
| 123 |
+
cursor = self.execute(
|
| 124 |
+
"""
|
| 125 |
+
INSERT INTO api_keys (name, key_prefix, secret_hash, enabled, created_at)
|
| 126 |
+
VALUES (?, ?, ?, 1, ?)
|
| 127 |
+
""",
|
| 128 |
+
(name, prefix, secret_hash, utcnow_iso()),
|
| 129 |
+
)
|
| 130 |
+
return int(cursor.lastrowid)
|
| 131 |
+
|
| 132 |
+
def find_api_key_by_prefix(self, prefix: str) -> sqlite3.Row | None:
|
| 133 |
+
return self.fetch_one("SELECT * FROM api_keys WHERE key_prefix = ?", (prefix,))
|
| 134 |
+
|
| 135 |
+
def touch_api_key(self, key_id: int) -> None:
|
| 136 |
+
self.execute(
|
| 137 |
+
"UPDATE api_keys SET last_used_at = ? WHERE id = ?",
|
| 138 |
+
(utcnow_iso(), key_id),
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
def list_api_keys(self) -> list[sqlite3.Row]:
|
| 142 |
+
return self.fetch_all(
|
| 143 |
+
"SELECT * FROM api_keys ORDER BY created_at DESC, id DESC"
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
def set_api_key_enabled(self, key_id: int, enabled: bool) -> None:
|
| 147 |
+
self.execute(
|
| 148 |
+
"UPDATE api_keys SET enabled = ? WHERE id = ?",
|
| 149 |
+
(1 if enabled else 0, key_id),
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
def delete_api_key(self, key_id: int) -> None:
|
| 153 |
+
self.execute("DELETE FROM api_keys WHERE id = ?", (key_id,))
|
| 154 |
+
|
| 155 |
+
def get_settings(self) -> dict[str, str]:
|
| 156 |
+
rows = self.fetch_all("SELECT name, value FROM app_settings")
|
| 157 |
+
return {row["name"]: row["value"] for row in rows}
|
| 158 |
+
|
| 159 |
+
def set_settings(self, values: dict[str, str]) -> None:
|
| 160 |
+
now = utcnow_iso()
|
| 161 |
+
with self._lock:
|
| 162 |
+
self._conn.executemany(
|
| 163 |
+
"""
|
| 164 |
+
INSERT INTO app_settings (name, value, updated_at)
|
| 165 |
+
VALUES (?, ?, ?)
|
| 166 |
+
ON CONFLICT(name) DO UPDATE SET
|
| 167 |
+
value = excluded.value,
|
| 168 |
+
updated_at = excluded.updated_at
|
| 169 |
+
""",
|
| 170 |
+
[(name, value, now) for name, value in values.items()],
|
| 171 |
+
)
|
| 172 |
+
self._conn.commit()
|
| 173 |
+
|
| 174 |
+
def create_job(
|
| 175 |
+
self,
|
| 176 |
+
*,
|
| 177 |
+
status: str,
|
| 178 |
+
source_filename: str,
|
| 179 |
+
input_sha256: str,
|
| 180 |
+
input_width: int,
|
| 181 |
+
input_height: int,
|
| 182 |
+
scale: int,
|
| 183 |
+
model_variant: str,
|
| 184 |
+
alpha: float,
|
| 185 |
+
tile_mode_requested: int,
|
| 186 |
+
cache_mode: int,
|
| 187 |
+
response_format: str,
|
| 188 |
+
) -> int:
|
| 189 |
+
cursor = self.execute(
|
| 190 |
+
"""
|
| 191 |
+
INSERT INTO jobs (
|
| 192 |
+
status, source_filename, input_sha256, input_width, input_height,
|
| 193 |
+
scale, model_variant, alpha, tile_mode_requested, cache_mode,
|
| 194 |
+
response_format, created_at
|
| 195 |
+
)
|
| 196 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 197 |
+
""",
|
| 198 |
+
(
|
| 199 |
+
status,
|
| 200 |
+
source_filename,
|
| 201 |
+
input_sha256,
|
| 202 |
+
input_width,
|
| 203 |
+
input_height,
|
| 204 |
+
scale,
|
| 205 |
+
model_variant,
|
| 206 |
+
alpha,
|
| 207 |
+
tile_mode_requested,
|
| 208 |
+
cache_mode,
|
| 209 |
+
response_format,
|
| 210 |
+
utcnow_iso(),
|
| 211 |
+
),
|
| 212 |
+
)
|
| 213 |
+
return int(cursor.lastrowid)
|
| 214 |
+
|
| 215 |
+
def mark_job_running(self, job_id: int) -> None:
|
| 216 |
+
self.execute(
|
| 217 |
+
"UPDATE jobs SET status = 'running', started_at = ? WHERE id = ?",
|
| 218 |
+
(utcnow_iso(), job_id),
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
def mark_job_success(
|
| 222 |
+
self,
|
| 223 |
+
job_id: int,
|
| 224 |
+
*,
|
| 225 |
+
output_width: int,
|
| 226 |
+
output_height: int,
|
| 227 |
+
tile_mode_used: int,
|
| 228 |
+
duration_ms: int,
|
| 229 |
+
) -> None:
|
| 230 |
+
self.execute(
|
| 231 |
+
"""
|
| 232 |
+
UPDATE jobs
|
| 233 |
+
SET status = 'succeeded',
|
| 234 |
+
output_width = ?,
|
| 235 |
+
output_height = ?,
|
| 236 |
+
tile_mode_used = ?,
|
| 237 |
+
duration_ms = ?,
|
| 238 |
+
finished_at = ?
|
| 239 |
+
WHERE id = ?
|
| 240 |
+
""",
|
| 241 |
+
(
|
| 242 |
+
output_width,
|
| 243 |
+
output_height,
|
| 244 |
+
tile_mode_used,
|
| 245 |
+
duration_ms,
|
| 246 |
+
utcnow_iso(),
|
| 247 |
+
job_id,
|
| 248 |
+
),
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
def mark_job_failed(self, job_id: int, error_message: str) -> None:
|
| 252 |
+
self.execute(
|
| 253 |
+
"""
|
| 254 |
+
UPDATE jobs
|
| 255 |
+
SET status = 'failed',
|
| 256 |
+
error_message = ?,
|
| 257 |
+
finished_at = ?
|
| 258 |
+
WHERE id = ?
|
| 259 |
+
""",
|
| 260 |
+
(error_message[:800], utcnow_iso(), job_id),
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
def list_recent_jobs(self, limit: int = 25) -> list[sqlite3.Row]:
|
| 264 |
+
return self.fetch_all(
|
| 265 |
+
"""
|
| 266 |
+
SELECT * FROM jobs
|
| 267 |
+
ORDER BY id DESC
|
| 268 |
+
LIMIT ?
|
| 269 |
+
""",
|
| 270 |
+
(limit,),
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
def dashboard_metrics(self) -> dict[str, Any]:
|
| 274 |
+
total_row = self.fetch_one(
|
| 275 |
+
"""
|
| 276 |
+
SELECT
|
| 277 |
+
COUNT(*) AS total,
|
| 278 |
+
SUM(CASE WHEN status = 'succeeded' THEN 1 ELSE 0 END) AS succeeded,
|
| 279 |
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
|
| 280 |
+
AVG(CASE WHEN duration_ms IS NOT NULL THEN duration_ms END) AS avg_duration_ms
|
| 281 |
+
FROM jobs
|
| 282 |
+
"""
|
| 283 |
+
)
|
| 284 |
+
last_row = self.fetch_one(
|
| 285 |
+
"""
|
| 286 |
+
SELECT finished_at
|
| 287 |
+
FROM jobs
|
| 288 |
+
WHERE status = 'succeeded'
|
| 289 |
+
ORDER BY id DESC
|
| 290 |
+
LIMIT 1
|
| 291 |
+
"""
|
| 292 |
+
)
|
| 293 |
+
return {
|
| 294 |
+
"total": int(total_row["total"] or 0),
|
| 295 |
+
"succeeded": int(total_row["succeeded"] or 0),
|
| 296 |
+
"failed": int(total_row["failed"] or 0),
|
| 297 |
+
"avg_duration_ms": int(total_row["avg_duration_ms"] or 0),
|
| 298 |
+
"last_success_at": last_row["finished_at"] if last_row else None,
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
def daily_job_counts(self, days: int = 7) -> list[dict[str, Any]]:
|
| 302 |
+
rows = self.fetch_all(
|
| 303 |
+
"""
|
| 304 |
+
SELECT substr(created_at, 1, 10) AS day, COUNT(*) AS count
|
| 305 |
+
FROM jobs
|
| 306 |
+
GROUP BY substr(created_at, 1, 10)
|
| 307 |
+
ORDER BY day DESC
|
| 308 |
+
LIMIT ?
|
| 309 |
+
""",
|
| 310 |
+
(days,),
|
| 311 |
+
)
|
| 312 |
+
ordered = list(reversed(rows))
|
| 313 |
+
return [{"day": row["day"], "count": int(row["count"])} for row in ordered]
|
app/main.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
|
| 8 |
+
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
|
| 9 |
+
from fastapi.staticfiles import StaticFiles
|
| 10 |
+
from fastapi.templating import Jinja2Templates
|
| 11 |
+
from starlette.middleware.sessions import SessionMiddleware
|
| 12 |
+
|
| 13 |
+
from .config import DEFAULT_RUNTIME_SETTINGS, MODEL_CATALOG, RUNTIME_FIELD_SPECS, load_env_settings
|
| 14 |
+
from .db import Database
|
| 15 |
+
from .runtime import BusyError, RealCuganRuntime, RuntimeConfigurationError, UnsupportedModelError
|
| 16 |
+
from .security import generate_api_key, generate_csrf_token, hash_secret, key_prefix, verify_secret
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
env = load_env_settings()
|
| 20 |
+
db = Database(env.database_path)
|
| 21 |
+
db.initialize()
|
| 22 |
+
runtime = RealCuganRuntime(env)
|
| 23 |
+
|
| 24 |
+
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent / "templates"))
|
| 25 |
+
|
| 26 |
+
app = FastAPI(title=env.app_name, docs_url=None, redoc_url=None)
|
| 27 |
+
app.add_middleware(
|
| 28 |
+
SessionMiddleware,
|
| 29 |
+
secret_key=env.session_secret,
|
| 30 |
+
same_site="lax",
|
| 31 |
+
https_only=env.session_https_only,
|
| 32 |
+
)
|
| 33 |
+
app.mount("/static", StaticFiles(directory=str(Path(__file__).resolve().parent / "static")), name="static")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def now_local_text() -> str:
|
| 37 |
+
return datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def initialize_defaults() -> None:
|
| 41 |
+
existing_settings = db.get_settings()
|
| 42 |
+
missing = {
|
| 43 |
+
name: value
|
| 44 |
+
for name, value in DEFAULT_RUNTIME_SETTINGS.items()
|
| 45 |
+
if name not in existing_settings
|
| 46 |
+
}
|
| 47 |
+
if missing:
|
| 48 |
+
db.set_settings(missing)
|
| 49 |
+
if env.admin_password:
|
| 50 |
+
user = db.get_user_by_username(env.admin_username)
|
| 51 |
+
if user is None:
|
| 52 |
+
db.ensure_admin_user(env.admin_username, hash_secret(env.admin_password))
|
| 53 |
+
if env.bootstrap_api_key:
|
| 54 |
+
prefix = key_prefix(env.bootstrap_api_key)
|
| 55 |
+
if db.find_api_key_by_prefix(prefix) is None:
|
| 56 |
+
db.create_api_key(env.bootstrap_api_key_name, prefix, hash_secret(env.bootstrap_api_key))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
initialize_defaults()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def get_current_user(request: Request) -> dict[str, Any] | None:
|
| 63 |
+
user_id = request.session.get("user_id")
|
| 64 |
+
if not user_id:
|
| 65 |
+
return None
|
| 66 |
+
row = db.get_user_by_id(int(user_id))
|
| 67 |
+
return dict(row) if row else None
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def ensure_admin(request: Request) -> dict[str, Any]:
|
| 71 |
+
user = get_current_user(request)
|
| 72 |
+
if user is None:
|
| 73 |
+
raise HTTPException(status_code=401, detail="Admin login required.")
|
| 74 |
+
return user
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def redirect_to_login() -> RedirectResponse:
|
| 78 |
+
return RedirectResponse("/admin/login", status_code=303)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def get_runtime_settings() -> dict[str, str]:
|
| 82 |
+
settings = DEFAULT_RUNTIME_SETTINGS.copy()
|
| 83 |
+
settings.update(db.get_settings())
|
| 84 |
+
return settings
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def typed_runtime_settings() -> dict[str, Any]:
|
| 88 |
+
raw = get_runtime_settings()
|
| 89 |
+
return {
|
| 90 |
+
"default_scale": int(raw["default_scale"]),
|
| 91 |
+
"default_variant": raw["default_variant"],
|
| 92 |
+
"default_alpha": float(raw["default_alpha"]),
|
| 93 |
+
"default_tile_mode": int(raw["default_tile_mode"]),
|
| 94 |
+
"default_cache_mode": int(raw["default_cache_mode"]),
|
| 95 |
+
"max_input_long_edge": int(raw["max_input_long_edge"]),
|
| 96 |
+
"max_input_pixels": int(raw["max_input_pixels"]),
|
| 97 |
+
"jpeg_quality": int(raw["jpeg_quality"]),
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def validate_settings_payload(payload: dict[str, str]) -> dict[str, str]:
|
| 102 |
+
scale = int(payload["default_scale"])
|
| 103 |
+
if scale not in {2, 3, 4}:
|
| 104 |
+
raise ValueError("Default scale must be 2, 3, or 4.")
|
| 105 |
+
variant = payload["default_variant"]
|
| 106 |
+
alpha = float(payload["default_alpha"])
|
| 107 |
+
tile_mode = int(payload["default_tile_mode"])
|
| 108 |
+
cache_mode = int(payload["default_cache_mode"])
|
| 109 |
+
max_edge = int(payload["max_input_long_edge"])
|
| 110 |
+
max_pixels = int(payload["max_input_pixels"])
|
| 111 |
+
jpeg_quality = int(payload["jpeg_quality"])
|
| 112 |
+
if alpha < 0.75 or alpha > 1.3:
|
| 113 |
+
raise ValueError("Alpha must stay between 0.75 and 1.3.")
|
| 114 |
+
if tile_mode < 0 or tile_mode > 5:
|
| 115 |
+
raise ValueError("Tile mode must be between 0 and 5.")
|
| 116 |
+
if cache_mode not in {0, 1}:
|
| 117 |
+
raise ValueError("Cache mode must be 0 or 1.")
|
| 118 |
+
if max_edge < 512 or max_edge > 4096:
|
| 119 |
+
raise ValueError("Max input long edge must stay between 512 and 4096.")
|
| 120 |
+
if max_pixels < 512 * 512 or max_pixels > 8_000_000:
|
| 121 |
+
raise ValueError("Max input pixels must stay between 262144 and 8000000.")
|
| 122 |
+
if jpeg_quality < 70 or jpeg_quality > 100:
|
| 123 |
+
raise ValueError("JPEG quality must stay between 70 and 100.")
|
| 124 |
+
normalized = {
|
| 125 |
+
"default_scale": str(scale),
|
| 126 |
+
"default_variant": variant,
|
| 127 |
+
"default_alpha": f"{alpha:.2f}",
|
| 128 |
+
"default_tile_mode": str(tile_mode),
|
| 129 |
+
"default_cache_mode": str(cache_mode),
|
| 130 |
+
"max_input_long_edge": str(max_edge),
|
| 131 |
+
"max_input_pixels": str(max_pixels),
|
| 132 |
+
"jpeg_quality": str(jpeg_quality),
|
| 133 |
+
}
|
| 134 |
+
if variant not in MODEL_CATALOG.get(scale, {}):
|
| 135 |
+
raise ValueError("Default model pair is not supported.")
|
| 136 |
+
return normalized
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def get_flash(request: Request) -> dict[str, str] | None:
|
| 140 |
+
flash = request.session.pop("flash", None)
|
| 141 |
+
return flash
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def set_flash(request: Request, level: str, message: str) -> None:
|
| 145 |
+
request.session["flash"] = {"level": level, "message": message}
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def csrf_token(request: Request) -> str:
|
| 149 |
+
token = request.session.get("csrf_token")
|
| 150 |
+
if not token:
|
| 151 |
+
token = generate_csrf_token()
|
| 152 |
+
request.session["csrf_token"] = token
|
| 153 |
+
return token
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def ensure_csrf(request: Request, token: str) -> None:
|
| 157 |
+
stored = request.session.get("csrf_token")
|
| 158 |
+
if not stored or token != stored:
|
| 159 |
+
raise HTTPException(status_code=400, detail="CSRF token mismatch.")
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def extract_api_key(request: Request) -> str:
|
| 163 |
+
authorization = request.headers.get("authorization", "")
|
| 164 |
+
if authorization.lower().startswith("bearer "):
|
| 165 |
+
return authorization.split(" ", 1)[1].strip()
|
| 166 |
+
return request.headers.get("x-api-key", "").strip()
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def authenticate_api_request(request: Request) -> dict[str, Any]:
|
| 170 |
+
secret = extract_api_key(request)
|
| 171 |
+
if not secret:
|
| 172 |
+
raise HTTPException(status_code=401, detail="Missing API key.")
|
| 173 |
+
row = db.find_api_key_by_prefix(key_prefix(secret))
|
| 174 |
+
if row is None or not row["enabled"]:
|
| 175 |
+
raise HTTPException(status_code=401, detail="Invalid API key.")
|
| 176 |
+
if not verify_secret(secret, row["secret_hash"]):
|
| 177 |
+
raise HTTPException(status_code=401, detail="Invalid API key.")
|
| 178 |
+
db.touch_api_key(int(row["id"]))
|
| 179 |
+
return dict(row)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def dashboard_context(request: Request, *, issued_key: str | None = None) -> dict[str, Any]:
|
| 183 |
+
settings = get_runtime_settings()
|
| 184 |
+
metrics = db.dashboard_metrics()
|
| 185 |
+
jobs = [dict(row) for row in db.list_recent_jobs(30)]
|
| 186 |
+
model_rows = runtime.available_models()
|
| 187 |
+
daily_counts = db.daily_job_counts(7)
|
| 188 |
+
grouped_models: dict[int, list[dict[str, Any]]] = {}
|
| 189 |
+
for row in model_rows:
|
| 190 |
+
grouped_models.setdefault(row["scale"], []).append(row)
|
| 191 |
+
max_daily_count = max((item["count"] for item in daily_counts), default=1)
|
| 192 |
+
return {
|
| 193 |
+
"request": request,
|
| 194 |
+
"app_name": env.app_name,
|
| 195 |
+
"flash": get_flash(request),
|
| 196 |
+
"user": get_current_user(request),
|
| 197 |
+
"csrf_token": csrf_token(request),
|
| 198 |
+
"settings": settings,
|
| 199 |
+
"settings_spec": RUNTIME_FIELD_SPECS,
|
| 200 |
+
"metrics": metrics,
|
| 201 |
+
"daily_counts": daily_counts,
|
| 202 |
+
"max_daily_count": max_daily_count,
|
| 203 |
+
"jobs": jobs,
|
| 204 |
+
"api_keys": [dict(row) for row in db.list_api_keys()],
|
| 205 |
+
"runtime_status": runtime.runtime_status(),
|
| 206 |
+
"models_by_scale": grouped_models,
|
| 207 |
+
"issued_key": issued_key or request.session.pop("issued_api_key", None),
|
| 208 |
+
"admin_configured": bool(db.get_user_by_username(env.admin_username)),
|
| 209 |
+
"server_time": now_local_text(),
|
| 210 |
+
"space_hint": {
|
| 211 |
+
"cpu_tier": "CPU Basic",
|
| 212 |
+
"cpu_spec": "2 vCPU / 16 GB RAM",
|
| 213 |
+
},
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
@app.get("/", response_class=JSONResponse)
|
| 218 |
+
async def root() -> JSONResponse:
|
| 219 |
+
return JSONResponse(
|
| 220 |
+
{
|
| 221 |
+
"name": env.app_name,
|
| 222 |
+
"admin": "/admin",
|
| 223 |
+
"health": "/healthz",
|
| 224 |
+
"api": "/api/v1/upscale",
|
| 225 |
+
}
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
@app.get("/healthz", response_class=JSONResponse)
|
| 230 |
+
async def healthz() -> JSONResponse:
|
| 231 |
+
return JSONResponse(
|
| 232 |
+
{
|
| 233 |
+
"status": "ok",
|
| 234 |
+
"busy": runtime.busy,
|
| 235 |
+
"models_loaded": runtime.runtime_status()["loaded_models"],
|
| 236 |
+
"admin_configured": bool(db.get_user_by_username(env.admin_username)),
|
| 237 |
+
}
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
@app.get("/admin/login", response_class=HTMLResponse)
|
| 242 |
+
async def admin_login_page(request: Request) -> HTMLResponse:
|
| 243 |
+
if get_current_user(request):
|
| 244 |
+
return RedirectResponse("/admin", status_code=303)
|
| 245 |
+
return templates.TemplateResponse(
|
| 246 |
+
request,
|
| 247 |
+
"login.html",
|
| 248 |
+
{
|
| 249 |
+
"app_name": env.app_name,
|
| 250 |
+
"flash": get_flash(request),
|
| 251 |
+
"csrf_token": csrf_token(request),
|
| 252 |
+
"admin_username": env.admin_username,
|
| 253 |
+
"admin_configured": bool(db.get_user_by_username(env.admin_username)),
|
| 254 |
+
},
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
@app.post("/admin/bootstrap")
|
| 259 |
+
async def admin_bootstrap(
|
| 260 |
+
request: Request,
|
| 261 |
+
password: str = Form(...),
|
| 262 |
+
confirm_password: str = Form(...),
|
| 263 |
+
csrf: str = Form(...),
|
| 264 |
+
) -> RedirectResponse:
|
| 265 |
+
ensure_csrf(request, csrf)
|
| 266 |
+
if db.get_user_by_username(env.admin_username) is not None:
|
| 267 |
+
set_flash(request, "error", "Admin account already exists.")
|
| 268 |
+
return RedirectResponse("/admin/login", status_code=303)
|
| 269 |
+
if len(password) < 10:
|
| 270 |
+
set_flash(request, "error", "Admin password must be at least 10 characters.")
|
| 271 |
+
return RedirectResponse("/admin/login", status_code=303)
|
| 272 |
+
if password != confirm_password:
|
| 273 |
+
set_flash(request, "error", "Password confirmation does not match.")
|
| 274 |
+
return RedirectResponse("/admin/login", status_code=303)
|
| 275 |
+
db.ensure_admin_user(env.admin_username, hash_secret(password))
|
| 276 |
+
user = db.get_user_by_username(env.admin_username)
|
| 277 |
+
request.session["user_id"] = int(user["id"])
|
| 278 |
+
set_flash(request, "success", "Admin account initialized.")
|
| 279 |
+
return RedirectResponse("/admin", status_code=303)
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
@app.post("/admin/login")
|
| 283 |
+
async def admin_login(
|
| 284 |
+
request: Request,
|
| 285 |
+
username: str = Form(...),
|
| 286 |
+
password: str = Form(...),
|
| 287 |
+
csrf: str = Form(...),
|
| 288 |
+
) -> RedirectResponse:
|
| 289 |
+
ensure_csrf(request, csrf)
|
| 290 |
+
row = db.get_user_by_username(username)
|
| 291 |
+
if row is None or not verify_secret(password, row["password_hash"]):
|
| 292 |
+
set_flash(request, "error", "Login failed. Check username and password.")
|
| 293 |
+
return RedirectResponse("/admin/login", status_code=303)
|
| 294 |
+
request.session["user_id"] = int(row["id"])
|
| 295 |
+
set_flash(request, "success", "Signed in.")
|
| 296 |
+
return RedirectResponse("/admin", status_code=303)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
@app.post("/admin/logout")
|
| 300 |
+
async def admin_logout(request: Request, csrf: str = Form(...)) -> RedirectResponse:
|
| 301 |
+
ensure_csrf(request, csrf)
|
| 302 |
+
request.session.clear()
|
| 303 |
+
set_flash(request, "success", "Signed out.")
|
| 304 |
+
return RedirectResponse("/admin/login", status_code=303)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
@app.get("/admin", response_class=HTMLResponse)
|
| 308 |
+
async def admin_dashboard(request: Request) -> HTMLResponse:
|
| 309 |
+
if get_current_user(request) is None:
|
| 310 |
+
return redirect_to_login()
|
| 311 |
+
return templates.TemplateResponse(request, "dashboard.html", dashboard_context(request))
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
@app.post("/admin/settings")
|
| 315 |
+
async def update_settings(request: Request) -> RedirectResponse:
|
| 316 |
+
if get_current_user(request) is None:
|
| 317 |
+
return redirect_to_login()
|
| 318 |
+
form = await request.form()
|
| 319 |
+
ensure_csrf(request, str(form.get("csrf", "")))
|
| 320 |
+
payload = {field["name"]: str(form.get(field["name"], "")).strip() for field in RUNTIME_FIELD_SPECS}
|
| 321 |
+
try:
|
| 322 |
+
normalized = validate_settings_payload(payload)
|
| 323 |
+
except ValueError as exc:
|
| 324 |
+
set_flash(request, "error", str(exc))
|
| 325 |
+
return RedirectResponse("/admin", status_code=303)
|
| 326 |
+
db.set_settings(normalized)
|
| 327 |
+
set_flash(request, "success", "Runtime settings updated.")
|
| 328 |
+
return RedirectResponse("/admin", status_code=303)
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
@app.post("/admin/password")
|
| 332 |
+
async def update_password(request: Request) -> RedirectResponse:
|
| 333 |
+
if get_current_user(request) is None:
|
| 334 |
+
return redirect_to_login()
|
| 335 |
+
form = await request.form()
|
| 336 |
+
ensure_csrf(request, str(form.get("csrf", "")))
|
| 337 |
+
current = str(form.get("current_password", ""))
|
| 338 |
+
new_password = str(form.get("new_password", ""))
|
| 339 |
+
confirm = str(form.get("confirm_password", ""))
|
| 340 |
+
user = db.get_user_by_username(env.admin_username)
|
| 341 |
+
if user is None or not verify_secret(current, user["password_hash"]):
|
| 342 |
+
set_flash(request, "error", "Current password is incorrect.")
|
| 343 |
+
return RedirectResponse("/admin", status_code=303)
|
| 344 |
+
if len(new_password) < 10:
|
| 345 |
+
set_flash(request, "error", "New password must be at least 10 characters.")
|
| 346 |
+
return RedirectResponse("/admin", status_code=303)
|
| 347 |
+
if new_password != confirm:
|
| 348 |
+
set_flash(request, "error", "New password confirmation does not match.")
|
| 349 |
+
return RedirectResponse("/admin", status_code=303)
|
| 350 |
+
db.update_admin_password(env.admin_username, hash_secret(new_password))
|
| 351 |
+
set_flash(request, "success", "Admin password updated.")
|
| 352 |
+
return RedirectResponse("/admin", status_code=303)
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
@app.post("/admin/api-keys")
|
| 356 |
+
async def create_api_key_admin(request: Request) -> RedirectResponse:
|
| 357 |
+
if get_current_user(request) is None:
|
| 358 |
+
return redirect_to_login()
|
| 359 |
+
form = await request.form()
|
| 360 |
+
ensure_csrf(request, str(form.get("csrf", "")))
|
| 361 |
+
name = str(form.get("name", "")).strip()
|
| 362 |
+
if len(name) < 2:
|
| 363 |
+
set_flash(request, "error", "API key name must be at least 2 characters.")
|
| 364 |
+
return RedirectResponse("/admin", status_code=303)
|
| 365 |
+
secret = generate_api_key()
|
| 366 |
+
db.create_api_key(name, key_prefix(secret), hash_secret(secret))
|
| 367 |
+
request.session["issued_api_key"] = secret
|
| 368 |
+
set_flash(request, "success", "New API key created. Copy it now, it will not be shown again.")
|
| 369 |
+
return RedirectResponse("/admin", status_code=303)
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
@app.post("/admin/api-keys/{key_id}/toggle")
|
| 373 |
+
async def toggle_api_key(key_id: int, request: Request) -> RedirectResponse:
|
| 374 |
+
if get_current_user(request) is None:
|
| 375 |
+
return redirect_to_login()
|
| 376 |
+
form = await request.form()
|
| 377 |
+
ensure_csrf(request, str(form.get("csrf", "")))
|
| 378 |
+
enabled = str(form.get("enabled", "0")) == "1"
|
| 379 |
+
db.set_api_key_enabled(key_id, enabled)
|
| 380 |
+
set_flash(request, "success", "API key status updated.")
|
| 381 |
+
return RedirectResponse("/admin", status_code=303)
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
@app.post("/admin/api-keys/{key_id}/delete")
|
| 385 |
+
async def delete_api_key(key_id: int, request: Request) -> RedirectResponse:
|
| 386 |
+
if get_current_user(request) is None:
|
| 387 |
+
return redirect_to_login()
|
| 388 |
+
form = await request.form()
|
| 389 |
+
ensure_csrf(request, str(form.get("csrf", "")))
|
| 390 |
+
db.delete_api_key(key_id)
|
| 391 |
+
set_flash(request, "success", "API key deleted.")
|
| 392 |
+
return RedirectResponse("/admin", status_code=303)
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
@app.get("/admin/metrics", response_class=JSONResponse)
|
| 396 |
+
async def admin_metrics(request: Request) -> JSONResponse:
|
| 397 |
+
if get_current_user(request) is None:
|
| 398 |
+
return JSONResponse({"detail": "Admin login required."}, status_code=401)
|
| 399 |
+
payload = {
|
| 400 |
+
"summary": db.dashboard_metrics(),
|
| 401 |
+
"daily_counts": db.daily_job_counts(14),
|
| 402 |
+
"runtime": runtime.runtime_status(),
|
| 403 |
+
"settings": get_runtime_settings(),
|
| 404 |
+
}
|
| 405 |
+
return JSONResponse(payload)
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
@app.post("/api/v1/upscale")
|
| 409 |
+
async def upscale_image(
|
| 410 |
+
request: Request,
|
| 411 |
+
file: UploadFile | None = File(None),
|
| 412 |
+
scale: int | None = Form(None),
|
| 413 |
+
variant: str | None = Form(None),
|
| 414 |
+
alpha: float | None = Form(None),
|
| 415 |
+
tile_mode: int | None = Form(None),
|
| 416 |
+
cache_mode: int | None = Form(None),
|
| 417 |
+
response_format: str = Form("png"),
|
| 418 |
+
) -> Response:
|
| 419 |
+
authenticate_api_request(request)
|
| 420 |
+
if file is None:
|
| 421 |
+
raise HTTPException(status_code=400, detail="Missing image file.")
|
| 422 |
+
settings = typed_runtime_settings()
|
| 423 |
+
chosen_scale = scale or settings["default_scale"]
|
| 424 |
+
chosen_variant = variant or settings["default_variant"]
|
| 425 |
+
chosen_alpha = alpha if alpha is not None else settings["default_alpha"]
|
| 426 |
+
chosen_tile = tile_mode if tile_mode is not None else settings["default_tile_mode"]
|
| 427 |
+
chosen_cache = cache_mode if cache_mode is not None else settings["default_cache_mode"]
|
| 428 |
+
chosen_format = response_format.strip().lower()
|
| 429 |
+
if chosen_format not in {"png", "jpeg", "webp"}:
|
| 430 |
+
raise HTTPException(status_code=400, detail="Unsupported response_format.")
|
| 431 |
+
payload = await file.read()
|
| 432 |
+
if not payload:
|
| 433 |
+
raise HTTPException(status_code=400, detail="Empty upload.")
|
| 434 |
+
try:
|
| 435 |
+
prepared = runtime.inspect_input(
|
| 436 |
+
payload,
|
| 437 |
+
max_input_long_edge=settings["max_input_long_edge"],
|
| 438 |
+
max_input_pixels=settings["max_input_pixels"],
|
| 439 |
+
)
|
| 440 |
+
except ValueError as exc:
|
| 441 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 442 |
+
job_id = db.create_job(
|
| 443 |
+
status="queued",
|
| 444 |
+
source_filename=file.filename or "upload",
|
| 445 |
+
input_sha256=prepared.input_sha256,
|
| 446 |
+
input_width=prepared.width,
|
| 447 |
+
input_height=prepared.height,
|
| 448 |
+
scale=chosen_scale,
|
| 449 |
+
model_variant=chosen_variant,
|
| 450 |
+
alpha=chosen_alpha,
|
| 451 |
+
tile_mode_requested=chosen_tile,
|
| 452 |
+
cache_mode=chosen_cache,
|
| 453 |
+
response_format=chosen_format,
|
| 454 |
+
)
|
| 455 |
+
db.mark_job_running(job_id)
|
| 456 |
+
try:
|
| 457 |
+
result = runtime.upscale(
|
| 458 |
+
prepared,
|
| 459 |
+
scale=chosen_scale,
|
| 460 |
+
variant=chosen_variant,
|
| 461 |
+
alpha=chosen_alpha,
|
| 462 |
+
tile_mode=chosen_tile,
|
| 463 |
+
cache_mode=chosen_cache,
|
| 464 |
+
response_format=chosen_format,
|
| 465 |
+
jpeg_quality=settings["jpeg_quality"],
|
| 466 |
+
)
|
| 467 |
+
except BusyError as exc:
|
| 468 |
+
db.mark_job_failed(job_id, str(exc))
|
| 469 |
+
raise HTTPException(status_code=429, detail=str(exc)) from exc
|
| 470 |
+
except (RuntimeConfigurationError, UnsupportedModelError) as exc:
|
| 471 |
+
db.mark_job_failed(job_id, str(exc))
|
| 472 |
+
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
| 473 |
+
except Exception as exc: # noqa: BLE001
|
| 474 |
+
db.mark_job_failed(job_id, str(exc))
|
| 475 |
+
raise HTTPException(status_code=500, detail="Upscaling failed.") from exc
|
| 476 |
+
db.mark_job_success(
|
| 477 |
+
job_id,
|
| 478 |
+
output_width=result.output_width,
|
| 479 |
+
output_height=result.output_height,
|
| 480 |
+
tile_mode_used=result.tile_mode_used,
|
| 481 |
+
duration_ms=result.duration_ms,
|
| 482 |
+
)
|
| 483 |
+
headers = {
|
| 484 |
+
"X-Job-Id": str(job_id),
|
| 485 |
+
"X-Scale": str(result.scale),
|
| 486 |
+
"X-Variant": result.variant,
|
| 487 |
+
"X-Tile-Mode-Used": str(result.tile_mode_used),
|
| 488 |
+
"X-Cache-Mode-Used": str(result.cache_mode_used),
|
| 489 |
+
"X-Duration-Ms": str(result.duration_ms),
|
| 490 |
+
}
|
| 491 |
+
return Response(content=result.content, media_type=result.media_type, headers=headers)
|
app/runtime.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
import hashlib
|
| 5 |
+
import importlib.util
|
| 6 |
+
import io
|
| 7 |
+
import threading
|
| 8 |
+
import time
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from types import ModuleType
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
from PIL import Image
|
| 15 |
+
import requests
|
| 16 |
+
|
| 17 |
+
from .config import EnvSettings, MODEL_CATALOG
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class RuntimeConfigurationError(RuntimeError):
|
| 21 |
+
"""Raised when Real-CUGAN runtime cannot be prepared."""
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class UnsupportedModelError(RuntimeError):
|
| 25 |
+
"""Raised when a scale or variant pair has no supported weight."""
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class BusyError(RuntimeError):
|
| 29 |
+
"""Raised when another inference is already running."""
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass(slots=True)
|
| 33 |
+
class PreparedImage:
|
| 34 |
+
input_sha256: str
|
| 35 |
+
rgb: np.ndarray
|
| 36 |
+
alpha: Image.Image | None
|
| 37 |
+
width: int
|
| 38 |
+
height: int
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclass(slots=True)
|
| 42 |
+
class UpscaleResult:
|
| 43 |
+
content: bytes
|
| 44 |
+
media_type: str
|
| 45 |
+
output_width: int
|
| 46 |
+
output_height: int
|
| 47 |
+
tile_mode_used: int
|
| 48 |
+
cache_mode_used: int
|
| 49 |
+
duration_ms: int
|
| 50 |
+
scale: int
|
| 51 |
+
variant: str
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class RealCuganRuntime:
|
| 55 |
+
def __init__(self, env: EnvSettings) -> None:
|
| 56 |
+
self.env = env
|
| 57 |
+
self._lock = threading.Lock()
|
| 58 |
+
self._module: ModuleType | None = None
|
| 59 |
+
self._models: dict[tuple[int, str], Any] = {}
|
| 60 |
+
self._last_runtime_error: str | None = None
|
| 61 |
+
|
| 62 |
+
@property
|
| 63 |
+
def busy(self) -> bool:
|
| 64 |
+
return self._lock.locked()
|
| 65 |
+
|
| 66 |
+
@property
|
| 67 |
+
def last_runtime_error(self) -> str | None:
|
| 68 |
+
return self._last_runtime_error
|
| 69 |
+
|
| 70 |
+
def inspect_input(
|
| 71 |
+
self,
|
| 72 |
+
payload: bytes,
|
| 73 |
+
*,
|
| 74 |
+
max_input_long_edge: int,
|
| 75 |
+
max_input_pixels: int,
|
| 76 |
+
) -> PreparedImage:
|
| 77 |
+
digest = hashlib.sha256(payload).hexdigest()
|
| 78 |
+
try:
|
| 79 |
+
with Image.open(io.BytesIO(payload)) as image:
|
| 80 |
+
image.load()
|
| 81 |
+
width, height = image.size
|
| 82 |
+
if width < 8 or height < 8:
|
| 83 |
+
raise ValueError("Image is too small. Minimum dimension is 8px.")
|
| 84 |
+
if max(width, height) > max_input_long_edge:
|
| 85 |
+
raise ValueError(
|
| 86 |
+
f"Image too large. Long edge {max(width, height)} exceeds limit {max_input_long_edge}."
|
| 87 |
+
)
|
| 88 |
+
if width * height > max_input_pixels:
|
| 89 |
+
raise ValueError(
|
| 90 |
+
f"Image too large. Pixel count {width * height} exceeds limit {max_input_pixels}."
|
| 91 |
+
)
|
| 92 |
+
rgba = image.convert("RGBA")
|
| 93 |
+
alpha = rgba.getchannel("A") if "A" in rgba.getbands() else None
|
| 94 |
+
rgb = np.asarray(rgba.convert("RGB"), dtype=np.uint8)
|
| 95 |
+
except OSError as exc:
|
| 96 |
+
raise ValueError("Unsupported or corrupted image file.") from exc
|
| 97 |
+
return PreparedImage(
|
| 98 |
+
input_sha256=digest,
|
| 99 |
+
rgb=rgb,
|
| 100 |
+
alpha=alpha,
|
| 101 |
+
width=width,
|
| 102 |
+
height=height,
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
def available_models(self) -> list[dict[str, Any]]:
|
| 106 |
+
rows: list[dict[str, Any]] = []
|
| 107 |
+
for scale, variants in MODEL_CATALOG.items():
|
| 108 |
+
for variant, candidates in variants.items():
|
| 109 |
+
resolved = next((name for name in candidates if (self.env.weights_dir / name).exists()), None)
|
| 110 |
+
rows.append(
|
| 111 |
+
{
|
| 112 |
+
"scale": scale,
|
| 113 |
+
"variant": variant,
|
| 114 |
+
"filename": resolved or candidates[0],
|
| 115 |
+
"present": bool(resolved),
|
| 116 |
+
"downloadable": bool(self.env.real_cugan_weight_base_url),
|
| 117 |
+
}
|
| 118 |
+
)
|
| 119 |
+
return rows
|
| 120 |
+
|
| 121 |
+
def runtime_status(self) -> dict[str, Any]:
|
| 122 |
+
vendor_path = self.env.vendor_dir / "upcunet_v3.py"
|
| 123 |
+
return {
|
| 124 |
+
"busy": self.busy,
|
| 125 |
+
"loaded_models": sorted(f"{scale}x/{variant}" for scale, variant in self._models),
|
| 126 |
+
"vendor_ready": vendor_path.exists(),
|
| 127 |
+
"vendor_path": str(vendor_path),
|
| 128 |
+
"last_runtime_error": self._last_runtime_error,
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
def upscale(
|
| 132 |
+
self,
|
| 133 |
+
prepared: PreparedImage,
|
| 134 |
+
*,
|
| 135 |
+
scale: int,
|
| 136 |
+
variant: str,
|
| 137 |
+
alpha: float,
|
| 138 |
+
tile_mode: int,
|
| 139 |
+
cache_mode: int,
|
| 140 |
+
response_format: str,
|
| 141 |
+
jpeg_quality: int,
|
| 142 |
+
) -> UpscaleResult:
|
| 143 |
+
if not self._lock.acquire(blocking=False):
|
| 144 |
+
raise BusyError("Another inference is already running.")
|
| 145 |
+
started = time.perf_counter()
|
| 146 |
+
try:
|
| 147 |
+
model = self._load_model(scale, variant)
|
| 148 |
+
tile_candidates = self._candidate_tile_modes(tile_mode)
|
| 149 |
+
cache_candidates = self._candidate_cache_modes(cache_mode)
|
| 150 |
+
last_exc: Exception | None = None
|
| 151 |
+
used_tile = tile_mode
|
| 152 |
+
used_cache = cache_mode
|
| 153 |
+
for cache_candidate in cache_candidates:
|
| 154 |
+
for tile_candidate in tile_candidates:
|
| 155 |
+
try:
|
| 156 |
+
output = model(
|
| 157 |
+
prepared.rgb,
|
| 158 |
+
tile_mode=tile_candidate,
|
| 159 |
+
cache_mode=cache_candidate,
|
| 160 |
+
alpha=alpha,
|
| 161 |
+
)
|
| 162 |
+
used_tile = tile_candidate
|
| 163 |
+
used_cache = cache_candidate
|
| 164 |
+
return self._serialize_output(
|
| 165 |
+
prepared=prepared,
|
| 166 |
+
output=output,
|
| 167 |
+
response_format=response_format,
|
| 168 |
+
jpeg_quality=jpeg_quality,
|
| 169 |
+
scale=scale,
|
| 170 |
+
variant=variant,
|
| 171 |
+
tile_mode_used=used_tile,
|
| 172 |
+
cache_mode_used=used_cache,
|
| 173 |
+
started=started,
|
| 174 |
+
)
|
| 175 |
+
except Exception as exc: # noqa: BLE001
|
| 176 |
+
last_exc = exc
|
| 177 |
+
if not self._should_retry(exc):
|
| 178 |
+
raise
|
| 179 |
+
continue
|
| 180 |
+
if last_exc:
|
| 181 |
+
raise last_exc
|
| 182 |
+
raise RuntimeConfigurationError("Inference failed before execution.")
|
| 183 |
+
except Exception as exc: # noqa: BLE001
|
| 184 |
+
self._last_runtime_error = str(exc)
|
| 185 |
+
raise
|
| 186 |
+
finally:
|
| 187 |
+
self._lock.release()
|
| 188 |
+
|
| 189 |
+
def _serialize_output(
|
| 190 |
+
self,
|
| 191 |
+
*,
|
| 192 |
+
prepared: PreparedImage,
|
| 193 |
+
output: np.ndarray,
|
| 194 |
+
response_format: str,
|
| 195 |
+
jpeg_quality: int,
|
| 196 |
+
scale: int,
|
| 197 |
+
variant: str,
|
| 198 |
+
tile_mode_used: int,
|
| 199 |
+
cache_mode_used: int,
|
| 200 |
+
started: float,
|
| 201 |
+
) -> UpscaleResult:
|
| 202 |
+
image = Image.fromarray(output.astype(np.uint8), mode="RGB")
|
| 203 |
+
if prepared.alpha is not None and response_format in {"png", "webp"}:
|
| 204 |
+
alpha = prepared.alpha.resize(image.size, Image.Resampling.LANCZOS)
|
| 205 |
+
image = image.convert("RGBA")
|
| 206 |
+
image.putalpha(alpha)
|
| 207 |
+
if response_format == "jpeg":
|
| 208 |
+
image = image.convert("RGB")
|
| 209 |
+
buffer = io.BytesIO()
|
| 210 |
+
save_kwargs: dict[str, Any] = {}
|
| 211 |
+
if response_format == "jpeg":
|
| 212 |
+
save_kwargs["quality"] = jpeg_quality
|
| 213 |
+
save_kwargs["optimize"] = True
|
| 214 |
+
if response_format == "webp":
|
| 215 |
+
save_kwargs["quality"] = jpeg_quality
|
| 216 |
+
save_kwargs["method"] = 6
|
| 217 |
+
image.save(buffer, format=response_format.upper(), **save_kwargs)
|
| 218 |
+
duration_ms = int((time.perf_counter() - started) * 1000)
|
| 219 |
+
media_type = {
|
| 220 |
+
"png": "image/png",
|
| 221 |
+
"jpeg": "image/jpeg",
|
| 222 |
+
"webp": "image/webp",
|
| 223 |
+
}[response_format]
|
| 224 |
+
return UpscaleResult(
|
| 225 |
+
content=buffer.getvalue(),
|
| 226 |
+
media_type=media_type,
|
| 227 |
+
output_width=image.width,
|
| 228 |
+
output_height=image.height,
|
| 229 |
+
tile_mode_used=tile_mode_used,
|
| 230 |
+
cache_mode_used=cache_mode_used,
|
| 231 |
+
duration_ms=duration_ms,
|
| 232 |
+
scale=scale,
|
| 233 |
+
variant=variant,
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
def _load_model(self, scale: int, variant: str) -> Any:
|
| 237 |
+
if scale not in MODEL_CATALOG or variant not in MODEL_CATALOG[scale]:
|
| 238 |
+
raise UnsupportedModelError(f"Unsupported model selection: {scale}x / {variant}")
|
| 239 |
+
key = (scale, variant)
|
| 240 |
+
cached = self._models.get(key)
|
| 241 |
+
if cached is not None:
|
| 242 |
+
return cached
|
| 243 |
+
module = self._load_vendor_module()
|
| 244 |
+
weight_path = self._resolve_weight_path(scale, variant)
|
| 245 |
+
try:
|
| 246 |
+
model = module.RealWaifuUpScaler(scale, str(weight_path), half=False, device="cpu")
|
| 247 |
+
except ModuleNotFoundError as exc:
|
| 248 |
+
raise RuntimeConfigurationError(
|
| 249 |
+
"PyTorch is not installed in the current environment."
|
| 250 |
+
) from exc
|
| 251 |
+
except Exception as exc: # noqa: BLE001
|
| 252 |
+
raise RuntimeConfigurationError(f"Failed to load weight {weight_path.name}: {exc}") from exc
|
| 253 |
+
self._models[key] = model
|
| 254 |
+
return model
|
| 255 |
+
|
| 256 |
+
def _load_vendor_module(self) -> ModuleType:
|
| 257 |
+
if self._module is not None:
|
| 258 |
+
return self._module
|
| 259 |
+
vendor_path = self._ensure_vendor_file()
|
| 260 |
+
spec = importlib.util.spec_from_file_location("real_cugan_vendor", vendor_path)
|
| 261 |
+
if spec is None or spec.loader is None:
|
| 262 |
+
raise RuntimeConfigurationError("Failed to prepare Real-CUGAN vendor module loader.")
|
| 263 |
+
module = importlib.util.module_from_spec(spec)
|
| 264 |
+
try:
|
| 265 |
+
spec.loader.exec_module(module)
|
| 266 |
+
except ModuleNotFoundError as exc:
|
| 267 |
+
raise RuntimeConfigurationError(
|
| 268 |
+
"Real-CUGAN runtime import failed. PyTorch is not installed in the current environment."
|
| 269 |
+
) from exc
|
| 270 |
+
except Exception as exc: # noqa: BLE001
|
| 271 |
+
raise RuntimeConfigurationError(f"Real-CUGAN vendor import failed: {exc}") from exc
|
| 272 |
+
self._module = module
|
| 273 |
+
return module
|
| 274 |
+
|
| 275 |
+
def _ensure_vendor_file(self) -> Path:
|
| 276 |
+
vendor_path = self.env.vendor_dir / "upcunet_v3.py"
|
| 277 |
+
if vendor_path.exists() and self._sha256(vendor_path) == self.env.real_cugan_sha256:
|
| 278 |
+
return vendor_path
|
| 279 |
+
try:
|
| 280 |
+
response = requests.get(self.env.real_cugan_upstream_url, timeout=45)
|
| 281 |
+
except requests.RequestException as exc:
|
| 282 |
+
raise RuntimeConfigurationError(
|
| 283 |
+
f"Failed to download Real-CUGAN upstream file: {exc}"
|
| 284 |
+
) from exc
|
| 285 |
+
if response.status_code != 200:
|
| 286 |
+
raise RuntimeConfigurationError(
|
| 287 |
+
f"Failed to download Real-CUGAN upstream file, status {response.status_code}."
|
| 288 |
+
)
|
| 289 |
+
payload = response.content
|
| 290 |
+
digest = hashlib.sha256(payload).hexdigest()
|
| 291 |
+
if digest != self.env.real_cugan_sha256:
|
| 292 |
+
raise RuntimeConfigurationError(
|
| 293 |
+
"Real-CUGAN upstream checksum mismatch. Refusing to execute unverified code."
|
| 294 |
+
)
|
| 295 |
+
vendor_path.write_bytes(payload)
|
| 296 |
+
return vendor_path
|
| 297 |
+
|
| 298 |
+
def _resolve_weight_path(self, scale: int, variant: str) -> Path:
|
| 299 |
+
candidates = MODEL_CATALOG[scale][variant]
|
| 300 |
+
for filename in candidates:
|
| 301 |
+
path = self.env.weights_dir / filename
|
| 302 |
+
if path.exists():
|
| 303 |
+
return path
|
| 304 |
+
if self.env.real_cugan_weight_base_url:
|
| 305 |
+
for filename in candidates:
|
| 306 |
+
downloaded = self._download_weight(filename)
|
| 307 |
+
if downloaded is not None:
|
| 308 |
+
return downloaded
|
| 309 |
+
raise RuntimeConfigurationError(
|
| 310 |
+
f"Weight for {scale}x / {variant} not found in {self.env.weights_dir}."
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
def _download_weight(self, filename: str) -> Path | None:
|
| 314 |
+
url = f"{self.env.real_cugan_weight_base_url}/{filename}"
|
| 315 |
+
try:
|
| 316 |
+
response = requests.get(url, timeout=120, stream=True)
|
| 317 |
+
except requests.RequestException:
|
| 318 |
+
return None
|
| 319 |
+
if response.status_code != 200:
|
| 320 |
+
return None
|
| 321 |
+
tmp_path = self.env.weights_dir / f".{filename}.part"
|
| 322 |
+
final_path = self.env.weights_dir / filename
|
| 323 |
+
with tmp_path.open("wb") as handle:
|
| 324 |
+
for chunk in response.iter_content(chunk_size=1024 * 1024):
|
| 325 |
+
if chunk:
|
| 326 |
+
handle.write(chunk)
|
| 327 |
+
tmp_path.replace(final_path)
|
| 328 |
+
return final_path
|
| 329 |
+
|
| 330 |
+
@staticmethod
|
| 331 |
+
def _sha256(path: Path) -> str:
|
| 332 |
+
digest = hashlib.sha256()
|
| 333 |
+
with path.open("rb") as handle:
|
| 334 |
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
| 335 |
+
digest.update(chunk)
|
| 336 |
+
return digest.hexdigest()
|
| 337 |
+
|
| 338 |
+
@staticmethod
|
| 339 |
+
def _candidate_tile_modes(tile_mode: int) -> list[int]:
|
| 340 |
+
ordered: list[int] = []
|
| 341 |
+
for value in [tile_mode, 5, 4, 3, 2, 1, 0]:
|
| 342 |
+
if value not in ordered:
|
| 343 |
+
ordered.append(value)
|
| 344 |
+
return ordered
|
| 345 |
+
|
| 346 |
+
@staticmethod
|
| 347 |
+
def _candidate_cache_modes(cache_mode: int) -> list[int]:
|
| 348 |
+
ordered: list[int] = []
|
| 349 |
+
for value in [cache_mode, 1, 0]:
|
| 350 |
+
if value not in ordered:
|
| 351 |
+
ordered.append(value)
|
| 352 |
+
return ordered
|
| 353 |
+
|
| 354 |
+
@staticmethod
|
| 355 |
+
def _should_retry(exc: Exception) -> bool:
|
| 356 |
+
if isinstance(exc, MemoryError):
|
| 357 |
+
return True
|
| 358 |
+
message = str(exc).lower()
|
| 359 |
+
retry_signals = [
|
| 360 |
+
"out of memory",
|
| 361 |
+
"can't allocate",
|
| 362 |
+
"cannot allocate",
|
| 363 |
+
"std::bad_alloc",
|
| 364 |
+
"not enough memory",
|
| 365 |
+
]
|
| 366 |
+
return any(signal in message for signal in retry_signals)
|
app/security.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import hashlib
|
| 5 |
+
import hmac
|
| 6 |
+
import secrets
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
PBKDF2_ROUNDS = 210_000
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _b64(data: bytes) -> str:
|
| 13 |
+
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _unb64(value: str) -> bytes:
|
| 17 |
+
padding = "=" * (-len(value) % 4)
|
| 18 |
+
return base64.urlsafe_b64decode(value + padding)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def hash_secret(secret: str) -> str:
|
| 22 |
+
salt = secrets.token_bytes(16)
|
| 23 |
+
digest = hashlib.pbkdf2_hmac("sha256", secret.encode("utf-8"), salt, PBKDF2_ROUNDS)
|
| 24 |
+
return f"pbkdf2_sha256${PBKDF2_ROUNDS}${_b64(salt)}${_b64(digest)}"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def verify_secret(secret: str, stored_hash: str) -> bool:
|
| 28 |
+
try:
|
| 29 |
+
algorithm, rounds_text, salt_text, digest_text = stored_hash.split("$", 3)
|
| 30 |
+
if algorithm != "pbkdf2_sha256":
|
| 31 |
+
return False
|
| 32 |
+
rounds = int(rounds_text)
|
| 33 |
+
salt = _unb64(salt_text)
|
| 34 |
+
expected = _unb64(digest_text)
|
| 35 |
+
except (ValueError, TypeError):
|
| 36 |
+
return False
|
| 37 |
+
actual = hashlib.pbkdf2_hmac("sha256", secret.encode("utf-8"), salt, rounds)
|
| 38 |
+
return hmac.compare_digest(actual, expected)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def generate_api_key() -> str:
|
| 42 |
+
return "rcg_" + secrets.token_urlsafe(32)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def key_prefix(secret: str) -> str:
|
| 46 |
+
return secret[:12]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def generate_csrf_token() -> str:
|
| 50 |
+
return secrets.token_urlsafe(24)
|
app/static/dashboard.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const shell = document.querySelector(".console-shell");
|
| 2 |
+
|
| 3 |
+
async function refreshMetrics() {
|
| 4 |
+
if (!shell) return;
|
| 5 |
+
const url = shell.dataset.metricsUrl;
|
| 6 |
+
if (!url) return;
|
| 7 |
+
try {
|
| 8 |
+
const response = await fetch(url, {
|
| 9 |
+
headers: {
|
| 10 |
+
"X-Requested-With": "dashboard",
|
| 11 |
+
},
|
| 12 |
+
credentials: "same-origin",
|
| 13 |
+
});
|
| 14 |
+
if (!response.ok) return;
|
| 15 |
+
const payload = await response.json();
|
| 16 |
+
for (const [name, value] of Object.entries(payload.summary || {})) {
|
| 17 |
+
const node = document.querySelector(`[data-metric="${name}"]`);
|
| 18 |
+
if (!node) continue;
|
| 19 |
+
node.textContent = name === "avg_duration_ms" ? `${value} ms` : String(value);
|
| 20 |
+
}
|
| 21 |
+
} catch (_) {
|
| 22 |
+
// Keep the dashboard quiet during background refresh failures.
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function bindCopyButtons() {
|
| 27 |
+
document.querySelectorAll("[data-copy-target]").forEach((button) => {
|
| 28 |
+
button.addEventListener("click", async () => {
|
| 29 |
+
const targetId = button.getAttribute("data-copy-target");
|
| 30 |
+
const target = targetId ? document.getElementById(targetId) : null;
|
| 31 |
+
if (!target) return;
|
| 32 |
+
const text = target.textContent || "";
|
| 33 |
+
try {
|
| 34 |
+
await navigator.clipboard.writeText(text);
|
| 35 |
+
button.textContent = "Copied";
|
| 36 |
+
setTimeout(() => {
|
| 37 |
+
button.textContent = "Copy key";
|
| 38 |
+
}, 1400);
|
| 39 |
+
} catch (_) {
|
| 40 |
+
button.textContent = "Copy failed";
|
| 41 |
+
}
|
| 42 |
+
});
|
| 43 |
+
});
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
bindCopyButtons();
|
| 47 |
+
refreshMetrics();
|
| 48 |
+
setInterval(refreshMetrics, 15000);
|
app/static/styles.css
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
color-scheme: light;
|
| 3 |
+
--bg: oklch(0.965 0.012 95);
|
| 4 |
+
--panel: oklch(0.99 0.004 95);
|
| 5 |
+
--panel-strong: oklch(0.973 0.008 95);
|
| 6 |
+
--line: oklch(0.885 0.016 88);
|
| 7 |
+
--line-strong: oklch(0.79 0.03 86);
|
| 8 |
+
--text: oklch(0.25 0.02 92);
|
| 9 |
+
--muted: oklch(0.48 0.018 90);
|
| 10 |
+
--accent: oklch(0.66 0.13 75);
|
| 11 |
+
--accent-soft: oklch(0.9 0.04 79);
|
| 12 |
+
--green: oklch(0.62 0.13 150);
|
| 13 |
+
--green-soft: oklch(0.91 0.03 150);
|
| 14 |
+
--red: oklch(0.6 0.17 26);
|
| 15 |
+
--red-soft: oklch(0.92 0.03 28);
|
| 16 |
+
--amber: oklch(0.72 0.15 80);
|
| 17 |
+
--amber-soft: oklch(0.93 0.03 83);
|
| 18 |
+
--shadow: 0 18px 38px rgba(67, 48, 21, 0.08);
|
| 19 |
+
--radius: 18px;
|
| 20 |
+
--radius-sm: 12px;
|
| 21 |
+
--content-width: 1420px;
|
| 22 |
+
--mono: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
|
| 23 |
+
--sans: "Public Sans", "Segoe UI", sans-serif;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
* {
|
| 27 |
+
box-sizing: border-box;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
html {
|
| 31 |
+
background: var(--bg);
|
| 32 |
+
color: var(--text);
|
| 33 |
+
font-family: var(--sans);
|
| 34 |
+
-webkit-font-smoothing: antialiased;
|
| 35 |
+
-moz-osx-font-smoothing: grayscale;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
body {
|
| 39 |
+
margin: 0;
|
| 40 |
+
min-height: 100vh;
|
| 41 |
+
background:
|
| 42 |
+
radial-gradient(circle at top right, rgba(215, 175, 81, 0.12), transparent 28%),
|
| 43 |
+
radial-gradient(circle at left 20% bottom 10%, rgba(109, 169, 121, 0.09), transparent 30%),
|
| 44 |
+
linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(246, 241, 233, 0.92));
|
| 45 |
+
color: var(--text);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
button,
|
| 49 |
+
input,
|
| 50 |
+
select,
|
| 51 |
+
textarea {
|
| 52 |
+
font: inherit;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
code,
|
| 56 |
+
pre,
|
| 57 |
+
.mono {
|
| 58 |
+
font-family: var(--mono);
|
| 59 |
+
font-variant-numeric: tabular-nums;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.eyebrow {
|
| 63 |
+
margin: 0 0 0.55rem;
|
| 64 |
+
color: var(--muted);
|
| 65 |
+
font-size: 0.78rem;
|
| 66 |
+
font-weight: 700;
|
| 67 |
+
letter-spacing: 0.12em;
|
| 68 |
+
text-transform: uppercase;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.console-shell,
|
| 72 |
+
.auth-layout {
|
| 73 |
+
width: min(calc(100% - 32px), var(--content-width));
|
| 74 |
+
margin: 0 auto;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.auth-shell {
|
| 78 |
+
display: grid;
|
| 79 |
+
place-items: center;
|
| 80 |
+
padding: 28px 0;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.auth-layout {
|
| 84 |
+
display: grid;
|
| 85 |
+
grid-template-columns: 1.2fr 0.9fr;
|
| 86 |
+
gap: 20px;
|
| 87 |
+
align-items: stretch;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.auth-panel,
|
| 91 |
+
.panel,
|
| 92 |
+
.summary-card,
|
| 93 |
+
.banner {
|
| 94 |
+
border: 1px solid rgba(121, 93, 48, 0.12);
|
| 95 |
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(250, 246, 239, 0.96));
|
| 96 |
+
border-radius: var(--radius);
|
| 97 |
+
box-shadow: var(--shadow);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.auth-panel {
|
| 101 |
+
padding: 28px;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.auth-panel--hero {
|
| 105 |
+
position: relative;
|
| 106 |
+
overflow: hidden;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.auth-panel--hero::after {
|
| 110 |
+
content: "";
|
| 111 |
+
position: absolute;
|
| 112 |
+
right: -50px;
|
| 113 |
+
top: -80px;
|
| 114 |
+
width: 240px;
|
| 115 |
+
height: 240px;
|
| 116 |
+
border-radius: 50%;
|
| 117 |
+
background: radial-gradient(circle, rgba(213, 168, 62, 0.24), rgba(213, 168, 62, 0));
|
| 118 |
+
pointer-events: none;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.auth-panel h1,
|
| 122 |
+
.topbar h1 {
|
| 123 |
+
margin: 0;
|
| 124 |
+
max-width: 12ch;
|
| 125 |
+
font-size: clamp(2.1rem, 4.2vw, 4rem);
|
| 126 |
+
line-height: 0.95;
|
| 127 |
+
letter-spacing: -0.045em;
|
| 128 |
+
text-wrap: balance;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.auth-copy,
|
| 132 |
+
.topbar-copy,
|
| 133 |
+
.panel-note,
|
| 134 |
+
.auth-panel p,
|
| 135 |
+
.summary-card p,
|
| 136 |
+
.runtime-list dd,
|
| 137 |
+
.empty-state {
|
| 138 |
+
color: var(--muted);
|
| 139 |
+
text-wrap: pretty;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.auth-grid {
|
| 143 |
+
display: grid;
|
| 144 |
+
gap: 14px;
|
| 145 |
+
margin-top: 24px;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.auth-grid article {
|
| 149 |
+
padding: 16px 18px;
|
| 150 |
+
border-radius: var(--radius-sm);
|
| 151 |
+
background: rgba(255, 251, 244, 0.72);
|
| 152 |
+
border: 1px solid rgba(125, 95, 46, 0.1);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.auth-grid strong,
|
| 156 |
+
.summary-card strong {
|
| 157 |
+
display: block;
|
| 158 |
+
margin: 6px 0;
|
| 159 |
+
font-size: 1.2rem;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.metric-label {
|
| 163 |
+
color: var(--muted);
|
| 164 |
+
font-size: 0.82rem;
|
| 165 |
+
font-weight: 700;
|
| 166 |
+
letter-spacing: 0.08em;
|
| 167 |
+
text-transform: uppercase;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.panel-heading {
|
| 171 |
+
display: flex;
|
| 172 |
+
justify-content: space-between;
|
| 173 |
+
gap: 16px;
|
| 174 |
+
align-items: flex-start;
|
| 175 |
+
margin-bottom: 18px;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.panel-heading h2 {
|
| 179 |
+
margin: 0;
|
| 180 |
+
font-size: 1.3rem;
|
| 181 |
+
line-height: 1;
|
| 182 |
+
letter-spacing: -0.03em;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.stack-form,
|
| 186 |
+
.settings-grid,
|
| 187 |
+
.inline-form {
|
| 188 |
+
display: grid;
|
| 189 |
+
gap: 14px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.settings-grid {
|
| 193 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.field,
|
| 197 |
+
.stack-form label {
|
| 198 |
+
display: grid;
|
| 199 |
+
gap: 8px;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.field span,
|
| 203 |
+
.stack-form span {
|
| 204 |
+
font-weight: 700;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
input,
|
| 208 |
+
select,
|
| 209 |
+
textarea {
|
| 210 |
+
width: 100%;
|
| 211 |
+
border: 1px solid var(--line);
|
| 212 |
+
border-radius: 12px;
|
| 213 |
+
background: rgba(255, 255, 255, 0.88);
|
| 214 |
+
padding: 0.86rem 0.95rem;
|
| 215 |
+
color: var(--text);
|
| 216 |
+
transition-property: border-color, box-shadow, background-color;
|
| 217 |
+
transition-duration: 160ms;
|
| 218 |
+
transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
input:focus-visible,
|
| 222 |
+
select:focus-visible,
|
| 223 |
+
textarea:focus-visible,
|
| 224 |
+
button:focus-visible {
|
| 225 |
+
outline: none;
|
| 226 |
+
border-color: rgba(190, 140, 33, 0.65);
|
| 227 |
+
box-shadow: 0 0 0 4px rgba(204, 156, 46, 0.12);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
input:disabled {
|
| 231 |
+
background: rgba(245, 238, 227, 0.92);
|
| 232 |
+
color: var(--muted);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.field small {
|
| 236 |
+
color: var(--muted);
|
| 237 |
+
font-size: 0.88rem;
|
| 238 |
+
line-height: 1.45;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.button {
|
| 242 |
+
display: inline-flex;
|
| 243 |
+
align-items: center;
|
| 244 |
+
justify-content: center;
|
| 245 |
+
gap: 8px;
|
| 246 |
+
min-height: 44px;
|
| 247 |
+
padding: 0 16px;
|
| 248 |
+
border: 1px solid transparent;
|
| 249 |
+
border-radius: 999px;
|
| 250 |
+
cursor: pointer;
|
| 251 |
+
font-weight: 700;
|
| 252 |
+
transition-property: transform, background-color, border-color, color, opacity;
|
| 253 |
+
transition-duration: 160ms;
|
| 254 |
+
transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
|
| 255 |
+
touch-action: manipulation;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.button:active {
|
| 259 |
+
transform: scale(0.97);
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.button--primary {
|
| 263 |
+
background: linear-gradient(180deg, rgba(211, 161, 45, 0.95), rgba(178, 125, 21, 0.98));
|
| 264 |
+
color: white;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.button--secondary {
|
| 268 |
+
background: rgba(223, 189, 118, 0.16);
|
| 269 |
+
border-color: rgba(183, 140, 41, 0.2);
|
| 270 |
+
color: var(--text);
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.button--ghost {
|
| 274 |
+
background: rgba(255, 255, 255, 0.7);
|
| 275 |
+
border-color: rgba(125, 95, 46, 0.16);
|
| 276 |
+
color: var(--text);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.button--danger {
|
| 280 |
+
background: rgba(188, 74, 54, 0.08);
|
| 281 |
+
border-color: rgba(188, 74, 54, 0.18);
|
| 282 |
+
color: oklch(0.42 0.14 23);
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.flash {
|
| 286 |
+
margin: 0 0 16px;
|
| 287 |
+
padding: 14px 16px;
|
| 288 |
+
border-radius: 14px;
|
| 289 |
+
font-weight: 600;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.flash--success {
|
| 293 |
+
background: var(--green-soft);
|
| 294 |
+
color: oklch(0.33 0.08 150);
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.flash--error {
|
| 298 |
+
background: var(--red-soft);
|
| 299 |
+
color: oklch(0.4 0.12 24);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.topbar {
|
| 303 |
+
display: flex;
|
| 304 |
+
justify-content: space-between;
|
| 305 |
+
gap: 20px;
|
| 306 |
+
align-items: flex-start;
|
| 307 |
+
padding: 28px 0 18px;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.topbar-actions {
|
| 311 |
+
display: flex;
|
| 312 |
+
align-items: center;
|
| 313 |
+
gap: 12px;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.status-chip,
|
| 317 |
+
.status-pill,
|
| 318 |
+
.inventory-state {
|
| 319 |
+
display: inline-flex;
|
| 320 |
+
align-items: center;
|
| 321 |
+
gap: 8px;
|
| 322 |
+
border-radius: 999px;
|
| 323 |
+
padding: 0.45rem 0.8rem;
|
| 324 |
+
font-size: 0.84rem;
|
| 325 |
+
font-weight: 700;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.status-chip {
|
| 329 |
+
background: rgba(255, 255, 255, 0.8);
|
| 330 |
+
border: 1px solid rgba(125, 95, 46, 0.12);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.status-chip--ok,
|
| 334 |
+
.inventory-state--ok,
|
| 335 |
+
.status-pill--succeeded {
|
| 336 |
+
color: oklch(0.35 0.09 150);
|
| 337 |
+
background: var(--green-soft);
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.status-chip--warn,
|
| 341 |
+
.status-pill--running,
|
| 342 |
+
.status-pill--queued {
|
| 343 |
+
color: oklch(0.43 0.1 80);
|
| 344 |
+
background: var(--amber-soft);
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.status-pill--failed,
|
| 348 |
+
.inventory-state--miss {
|
| 349 |
+
color: oklch(0.42 0.13 24);
|
| 350 |
+
background: var(--red-soft);
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.status-dot {
|
| 354 |
+
width: 8px;
|
| 355 |
+
height: 8px;
|
| 356 |
+
border-radius: 999px;
|
| 357 |
+
background: currentColor;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
.banner {
|
| 361 |
+
display: flex;
|
| 362 |
+
justify-content: space-between;
|
| 363 |
+
gap: 18px;
|
| 364 |
+
align-items: center;
|
| 365 |
+
padding: 18px 20px;
|
| 366 |
+
margin-bottom: 18px;
|
| 367 |
+
background: linear-gradient(135deg, rgba(255, 246, 220, 0.98), rgba(246, 254, 243, 0.98));
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.banner h2 {
|
| 371 |
+
margin: 0;
|
| 372 |
+
font-size: 1.15rem;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.key-display {
|
| 376 |
+
display: grid;
|
| 377 |
+
gap: 10px;
|
| 378 |
+
justify-items: end;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.key-display code {
|
| 382 |
+
display: inline-block;
|
| 383 |
+
max-width: min(100%, 520px);
|
| 384 |
+
overflow-wrap: anywhere;
|
| 385 |
+
padding: 12px 14px;
|
| 386 |
+
border-radius: 14px;
|
| 387 |
+
background: rgba(255, 255, 255, 0.78);
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.summary-grid,
|
| 391 |
+
.main-grid,
|
| 392 |
+
.control-grid {
|
| 393 |
+
display: grid;
|
| 394 |
+
gap: 18px;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.summary-grid {
|
| 398 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.summary-card,
|
| 402 |
+
.panel {
|
| 403 |
+
padding: 20px;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.summary-card strong {
|
| 407 |
+
font-size: clamp(1.7rem, 2vw, 2.4rem);
|
| 408 |
+
letter-spacing: -0.04em;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
.main-grid {
|
| 412 |
+
grid-template-columns: minmax(0, 1.7fr) minmax(320px, 0.9fr);
|
| 413 |
+
margin-top: 18px;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.control-grid {
|
| 417 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 418 |
+
margin: 18px 0 32px;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.bar-strip {
|
| 422 |
+
display: grid;
|
| 423 |
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
| 424 |
+
gap: 12px;
|
| 425 |
+
min-height: 180px;
|
| 426 |
+
align-items: end;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.bar-item {
|
| 430 |
+
display: grid;
|
| 431 |
+
justify-items: center;
|
| 432 |
+
gap: 8px;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.bar-track {
|
| 436 |
+
position: relative;
|
| 437 |
+
width: 100%;
|
| 438 |
+
min-height: 140px;
|
| 439 |
+
border-radius: 16px;
|
| 440 |
+
background: linear-gradient(180deg, rgba(239, 230, 207, 0.5), rgba(255, 255, 255, 0.85));
|
| 441 |
+
border: 1px solid rgba(126, 96, 47, 0.08);
|
| 442 |
+
overflow: hidden;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
.bar-fill {
|
| 446 |
+
position: absolute;
|
| 447 |
+
inset: auto 0 0 0;
|
| 448 |
+
min-height: 10px;
|
| 449 |
+
background: linear-gradient(180deg, rgba(221, 176, 62, 0.92), rgba(170, 117, 24, 0.96));
|
| 450 |
+
border-radius: 12px 12px 0 0;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
.table-wrap {
|
| 454 |
+
overflow-x: auto;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
.data-table {
|
| 458 |
+
width: 100%;
|
| 459 |
+
border-collapse: collapse;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
.data-table th,
|
| 463 |
+
.data-table td {
|
| 464 |
+
padding: 12px 10px;
|
| 465 |
+
border-bottom: 1px solid rgba(89, 67, 35, 0.08);
|
| 466 |
+
vertical-align: top;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
.data-table th {
|
| 470 |
+
color: var(--muted);
|
| 471 |
+
font-size: 0.82rem;
|
| 472 |
+
font-weight: 700;
|
| 473 |
+
letter-spacing: 0.08em;
|
| 474 |
+
text-transform: uppercase;
|
| 475 |
+
text-align: left;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.data-table td:nth-child(1),
|
| 479 |
+
.data-table td:nth-child(3),
|
| 480 |
+
.data-table td:nth-child(4),
|
| 481 |
+
.data-table td:nth-child(5),
|
| 482 |
+
.data-table td:nth-child(6),
|
| 483 |
+
.data-table td:nth-child(7) {
|
| 484 |
+
text-align: right;
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
.error-cell {
|
| 488 |
+
max-width: 280px;
|
| 489 |
+
color: var(--muted);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.runtime-list {
|
| 493 |
+
display: grid;
|
| 494 |
+
gap: 12px;
|
| 495 |
+
margin: 0;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.runtime-list div {
|
| 499 |
+
padding: 12px 14px;
|
| 500 |
+
border-radius: 14px;
|
| 501 |
+
background: rgba(255, 252, 247, 0.8);
|
| 502 |
+
border: 1px solid rgba(126, 96, 47, 0.08);
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
.runtime-list dt {
|
| 506 |
+
margin-bottom: 6px;
|
| 507 |
+
color: var(--muted);
|
| 508 |
+
font-size: 0.82rem;
|
| 509 |
+
font-weight: 700;
|
| 510 |
+
letter-spacing: 0.08em;
|
| 511 |
+
text-transform: uppercase;
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
.runtime-list dd {
|
| 515 |
+
margin: 0;
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
.runtime-error {
|
| 519 |
+
overflow-wrap: anywhere;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
.inventory-block + .inventory-block {
|
| 523 |
+
margin-top: 18px;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
.inventory-block h3 {
|
| 527 |
+
margin: 0 0 10px;
|
| 528 |
+
font-size: 1rem;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
.inventory-list {
|
| 532 |
+
display: grid;
|
| 533 |
+
gap: 10px;
|
| 534 |
+
padding: 0;
|
| 535 |
+
margin: 0;
|
| 536 |
+
list-style: none;
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
.inventory-list li {
|
| 540 |
+
display: flex;
|
| 541 |
+
justify-content: space-between;
|
| 542 |
+
gap: 12px;
|
| 543 |
+
align-items: center;
|
| 544 |
+
padding: 12px 14px;
|
| 545 |
+
border-radius: 14px;
|
| 546 |
+
background: rgba(255, 252, 247, 0.78);
|
| 547 |
+
border: 1px solid rgba(126, 96, 47, 0.08);
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.inventory-list code {
|
| 551 |
+
display: block;
|
| 552 |
+
margin-top: 4px;
|
| 553 |
+
font-size: 0.82rem;
|
| 554 |
+
color: var(--muted);
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
.inline-form {
|
| 558 |
+
grid-template-columns: minmax(0, 1fr) auto;
|
| 559 |
+
align-items: end;
|
| 560 |
+
margin-bottom: 16px;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
.action-row {
|
| 564 |
+
display: flex;
|
| 565 |
+
gap: 8px;
|
| 566 |
+
justify-content: flex-end;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
.code-block {
|
| 570 |
+
margin: 0;
|
| 571 |
+
padding: 16px;
|
| 572 |
+
border-radius: 14px;
|
| 573 |
+
background: oklch(0.205 0.02 85);
|
| 574 |
+
color: oklch(0.93 0.02 96);
|
| 575 |
+
overflow-x: auto;
|
| 576 |
+
line-height: 1.55;
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
.empty-state {
|
| 580 |
+
padding: 18px 6px;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
@media (hover: hover) {
|
| 584 |
+
.button--ghost:hover,
|
| 585 |
+
.button--secondary:hover,
|
| 586 |
+
.button--danger:hover {
|
| 587 |
+
border-color: rgba(163, 123, 35, 0.28);
|
| 588 |
+
background: rgba(255, 255, 255, 0.95);
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
.button--primary:hover {
|
| 592 |
+
filter: brightness(1.02);
|
| 593 |
+
}
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
@media (max-width: 1080px) {
|
| 597 |
+
.auth-layout,
|
| 598 |
+
.main-grid,
|
| 599 |
+
.control-grid,
|
| 600 |
+
.summary-grid {
|
| 601 |
+
grid-template-columns: 1fr;
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
.settings-grid {
|
| 605 |
+
grid-template-columns: 1fr;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
.topbar,
|
| 609 |
+
.banner {
|
| 610 |
+
flex-direction: column;
|
| 611 |
+
align-items: stretch;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.key-display {
|
| 615 |
+
justify-items: stretch;
|
| 616 |
+
}
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
@media (max-width: 720px) {
|
| 620 |
+
.console-shell,
|
| 621 |
+
.auth-layout {
|
| 622 |
+
width: min(calc(100% - 20px), var(--content-width));
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
.auth-panel,
|
| 626 |
+
.panel,
|
| 627 |
+
.summary-card,
|
| 628 |
+
.banner {
|
| 629 |
+
padding: 16px;
|
| 630 |
+
border-radius: 16px;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
.bar-strip {
|
| 634 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
.data-table td:nth-child(1),
|
| 638 |
+
.data-table td:nth-child(3),
|
| 639 |
+
.data-table td:nth-child(4),
|
| 640 |
+
.data-table td:nth-child(5),
|
| 641 |
+
.data-table td:nth-child(6),
|
| 642 |
+
.data-table td:nth-child(7) {
|
| 643 |
+
text-align: left;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
.action-row {
|
| 647 |
+
flex-direction: column;
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
.inline-form {
|
| 651 |
+
grid-template-columns: 1fr;
|
| 652 |
+
}
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
@media (prefers-reduced-motion: reduce) {
|
| 656 |
+
*,
|
| 657 |
+
*::before,
|
| 658 |
+
*::after {
|
| 659 |
+
animation: none !important;
|
| 660 |
+
transition-duration: 0.01ms !important;
|
| 661 |
+
scroll-behavior: auto !important;
|
| 662 |
+
}
|
| 663 |
+
}
|
app/templates/base.html
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" />
|
| 6 |
+
<title>{% block title %}{{ app_name }}{% endblock %}</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link
|
| 10 |
+
href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap"
|
| 11 |
+
rel="stylesheet"
|
| 12 |
+
/>
|
| 13 |
+
<link rel="stylesheet" href="{{ request.url_for('static', path='/styles.css') }}" />
|
| 14 |
+
{% block head %}{% endblock %}
|
| 15 |
+
</head>
|
| 16 |
+
<body class="{% block body_class %}{% endblock %}">
|
| 17 |
+
{% block body %}{% endblock %}
|
| 18 |
+
</body>
|
| 19 |
+
</html>
|
app/templates/dashboard.html
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}{{ app_name }} | Dashboard{% endblock %}
|
| 4 |
+
{% block head %}
|
| 5 |
+
<script defer src="{{ request.url_for('static', path='/dashboard.js') }}"></script>
|
| 6 |
+
{% endblock %}
|
| 7 |
+
|
| 8 |
+
{% block body %}
|
| 9 |
+
<main class="console-shell" data-metrics-url="/admin/metrics">
|
| 10 |
+
<header class="topbar">
|
| 11 |
+
<div>
|
| 12 |
+
<p class="eyebrow">Operations console</p>
|
| 13 |
+
<h1>{{ app_name }}</h1>
|
| 14 |
+
<p class="topbar-copy">
|
| 15 |
+
Real-CUGAN control plane for serial CPU inference. Last server refresh: {{ server_time }}.
|
| 16 |
+
</p>
|
| 17 |
+
</div>
|
| 18 |
+
<div class="topbar-actions">
|
| 19 |
+
<div class="status-chip {% if runtime_status.busy %}status-chip--warn{% else %}status-chip--ok{% endif %}">
|
| 20 |
+
<span class="status-dot"></span>
|
| 21 |
+
{% if runtime_status.busy %}Job running{% else %}Idle{% endif %}
|
| 22 |
+
</div>
|
| 23 |
+
<form method="post" action="/admin/logout">
|
| 24 |
+
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
| 25 |
+
<button type="submit" class="button button--ghost">Sign out</button>
|
| 26 |
+
</form>
|
| 27 |
+
</div>
|
| 28 |
+
</header>
|
| 29 |
+
|
| 30 |
+
{% if flash %}
|
| 31 |
+
<div class="flash flash--{{ flash.level }}">{{ flash.message }}</div>
|
| 32 |
+
{% endif %}
|
| 33 |
+
|
| 34 |
+
{% if issued_key %}
|
| 35 |
+
<section class="banner banner--key">
|
| 36 |
+
<div>
|
| 37 |
+
<p class="eyebrow">New API key</p>
|
| 38 |
+
<h2>Copy it now</h2>
|
| 39 |
+
<p>This secret is shown only once. Save it in your client or secret manager before refreshing.</p>
|
| 40 |
+
</div>
|
| 41 |
+
<div class="key-display">
|
| 42 |
+
<code id="issued-api-key">{{ issued_key }}</code>
|
| 43 |
+
<button type="button" class="button button--secondary" data-copy-target="issued-api-key">
|
| 44 |
+
Copy key
|
| 45 |
+
</button>
|
| 46 |
+
</div>
|
| 47 |
+
</section>
|
| 48 |
+
{% endif %}
|
| 49 |
+
|
| 50 |
+
<section class="summary-grid">
|
| 51 |
+
<article class="summary-card">
|
| 52 |
+
<span class="metric-label">Total jobs</span>
|
| 53 |
+
<strong data-metric="total">{{ metrics.total }}</strong>
|
| 54 |
+
<p>All requests recorded in local SQLite state.</p>
|
| 55 |
+
</article>
|
| 56 |
+
<article class="summary-card">
|
| 57 |
+
<span class="metric-label">Succeeded</span>
|
| 58 |
+
<strong data-metric="succeeded">{{ metrics.succeeded }}</strong>
|
| 59 |
+
<p>Successful renders returned to API clients.</p>
|
| 60 |
+
</article>
|
| 61 |
+
<article class="summary-card">
|
| 62 |
+
<span class="metric-label">Failed</span>
|
| 63 |
+
<strong data-metric="failed">{{ metrics.failed }}</strong>
|
| 64 |
+
<p>Includes rejected images and runtime configuration errors.</p>
|
| 65 |
+
</article>
|
| 66 |
+
<article class="summary-card">
|
| 67 |
+
<span class="metric-label">Average duration</span>
|
| 68 |
+
<strong data-metric="avg_duration_ms">{{ metrics.avg_duration_ms }} ms</strong>
|
| 69 |
+
<p>Measured across completed jobs with duration available.</p>
|
| 70 |
+
</article>
|
| 71 |
+
</section>
|
| 72 |
+
|
| 73 |
+
<section class="main-grid">
|
| 74 |
+
<div class="main-column">
|
| 75 |
+
<section class="panel panel--chart">
|
| 76 |
+
<div class="panel-heading">
|
| 77 |
+
<div>
|
| 78 |
+
<p class="eyebrow">Activity</p>
|
| 79 |
+
<h2>Daily job rhythm</h2>
|
| 80 |
+
</div>
|
| 81 |
+
<p class="panel-note">Seven-day view, useful for checking whether the Space is actually being used.</p>
|
| 82 |
+
</div>
|
| 83 |
+
<div class="bar-strip" id="daily-chart">
|
| 84 |
+
{% for item in daily_counts %}
|
| 85 |
+
<div class="bar-item">
|
| 86 |
+
<div class="bar-track">
|
| 87 |
+
<span
|
| 88 |
+
class="bar-fill"
|
| 89 |
+
style="height: {{ ((item.count / max_daily_count) * 100)|round(2) if max_daily_count else 0 }}%;"
|
| 90 |
+
></span>
|
| 91 |
+
</div>
|
| 92 |
+
<strong>{{ item.count }}</strong>
|
| 93 |
+
<span>{{ item.day[5:] }}</span>
|
| 94 |
+
</div>
|
| 95 |
+
{% else %}
|
| 96 |
+
<div class="empty-state">No jobs recorded yet.</div>
|
| 97 |
+
{% endfor %}
|
| 98 |
+
</div>
|
| 99 |
+
</section>
|
| 100 |
+
|
| 101 |
+
<section class="panel">
|
| 102 |
+
<div class="panel-heading">
|
| 103 |
+
<div>
|
| 104 |
+
<p class="eyebrow">Recent jobs</p>
|
| 105 |
+
<h2>Execution log</h2>
|
| 106 |
+
</div>
|
| 107 |
+
<p class="panel-note">Last 30 jobs, newest first.</p>
|
| 108 |
+
</div>
|
| 109 |
+
<div class="table-wrap">
|
| 110 |
+
<table class="data-table">
|
| 111 |
+
<thead>
|
| 112 |
+
<tr>
|
| 113 |
+
<th>ID</th>
|
| 114 |
+
<th>Status</th>
|
| 115 |
+
<th>Input</th>
|
| 116 |
+
<th>Model</th>
|
| 117 |
+
<th>Tile</th>
|
| 118 |
+
<th>Duration</th>
|
| 119 |
+
<th>Created</th>
|
| 120 |
+
<th>Error</th>
|
| 121 |
+
</tr>
|
| 122 |
+
</thead>
|
| 123 |
+
<tbody>
|
| 124 |
+
{% for job in jobs %}
|
| 125 |
+
<tr>
|
| 126 |
+
<td class="mono">#{{ job.id }}</td>
|
| 127 |
+
<td>
|
| 128 |
+
<span class="status-pill status-pill--{{ job.status }}">{{ job.status }}</span>
|
| 129 |
+
</td>
|
| 130 |
+
<td class="mono">
|
| 131 |
+
{{ job.input_width or "-" }}×{{ job.input_height or "-" }}
|
| 132 |
+
</td>
|
| 133 |
+
<td class="mono">{{ job.scale }}x / {{ job.model_variant }}</td>
|
| 134 |
+
<td class="mono">{{ job.tile_mode_used if job.tile_mode_used is not none else job.tile_mode_requested }}</td>
|
| 135 |
+
<td class="mono">{{ job.duration_ms if job.duration_ms is not none else "-" }}</td>
|
| 136 |
+
<td class="mono">{{ job.created_at[:19].replace("T", " ") }}</td>
|
| 137 |
+
<td class="error-cell">{{ job.error_message or "-" }}</td>
|
| 138 |
+
</tr>
|
| 139 |
+
{% else %}
|
| 140 |
+
<tr>
|
| 141 |
+
<td colspan="8" class="empty-state">No jobs recorded yet.</td>
|
| 142 |
+
</tr>
|
| 143 |
+
{% endfor %}
|
| 144 |
+
</tbody>
|
| 145 |
+
</table>
|
| 146 |
+
</div>
|
| 147 |
+
</section>
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
<aside class="side-column">
|
| 151 |
+
<section class="panel panel--status">
|
| 152 |
+
<div class="panel-heading">
|
| 153 |
+
<div>
|
| 154 |
+
<p class="eyebrow">Runtime</p>
|
| 155 |
+
<h2>Service posture</h2>
|
| 156 |
+
</div>
|
| 157 |
+
<p class="panel-note">{{ space_hint.cpu_tier }}, {{ space_hint.cpu_spec }}</p>
|
| 158 |
+
</div>
|
| 159 |
+
<dl class="runtime-list">
|
| 160 |
+
<div>
|
| 161 |
+
<dt>Inference lock</dt>
|
| 162 |
+
<dd>{% if runtime_status.busy %}Busy{% else %}Open{% endif %}</dd>
|
| 163 |
+
</div>
|
| 164 |
+
<div>
|
| 165 |
+
<dt>Vendor code</dt>
|
| 166 |
+
<dd>{% if runtime_status.vendor_ready %}Cached{% else %}Lazy fetch{% endif %}</dd>
|
| 167 |
+
</div>
|
| 168 |
+
<div>
|
| 169 |
+
<dt>Loaded models</dt>
|
| 170 |
+
<dd>
|
| 171 |
+
{% if runtime_status.loaded_models %}
|
| 172 |
+
{{ runtime_status.loaded_models|join(", ") }}
|
| 173 |
+
{% else %}
|
| 174 |
+
None yet
|
| 175 |
+
{% endif %}
|
| 176 |
+
</dd>
|
| 177 |
+
</div>
|
| 178 |
+
<div>
|
| 179 |
+
<dt>Last success</dt>
|
| 180 |
+
<dd>{{ metrics.last_success_at or "None yet" }}</dd>
|
| 181 |
+
</div>
|
| 182 |
+
<div>
|
| 183 |
+
<dt>Last runtime error</dt>
|
| 184 |
+
<dd class="runtime-error">{{ runtime_status.last_runtime_error or "None" }}</dd>
|
| 185 |
+
</div>
|
| 186 |
+
</dl>
|
| 187 |
+
</section>
|
| 188 |
+
|
| 189 |
+
<section class="panel">
|
| 190 |
+
<div class="panel-heading">
|
| 191 |
+
<div>
|
| 192 |
+
<p class="eyebrow">Models</p>
|
| 193 |
+
<h2>Weight inventory</h2>
|
| 194 |
+
</div>
|
| 195 |
+
<p class="panel-note">Each row maps to a file expected in the local <code>weights/</code> directory.</p>
|
| 196 |
+
</div>
|
| 197 |
+
{% for scale, items in models_by_scale.items()|sort %}
|
| 198 |
+
<div class="inventory-block">
|
| 199 |
+
<h3>{{ scale }}x</h3>
|
| 200 |
+
<ul class="inventory-list">
|
| 201 |
+
{% for item in items %}
|
| 202 |
+
<li>
|
| 203 |
+
<div>
|
| 204 |
+
<strong>{{ item.variant }}</strong>
|
| 205 |
+
<code>{{ item.filename }}</code>
|
| 206 |
+
</div>
|
| 207 |
+
<span class="inventory-state {% if item.present %}inventory-state--ok{% else %}inventory-state--miss{% endif %}">
|
| 208 |
+
{% if item.present %}present{% else %}missing{% endif %}
|
| 209 |
+
</span>
|
| 210 |
+
</li>
|
| 211 |
+
{% endfor %}
|
| 212 |
+
</ul>
|
| 213 |
+
</div>
|
| 214 |
+
{% endfor %}
|
| 215 |
+
</section>
|
| 216 |
+
</aside>
|
| 217 |
+
</section>
|
| 218 |
+
|
| 219 |
+
<section class="control-grid">
|
| 220 |
+
<section class="panel">
|
| 221 |
+
<div class="panel-heading">
|
| 222 |
+
<div>
|
| 223 |
+
<p class="eyebrow">Runtime controls</p>
|
| 224 |
+
<h2>Default inference profile</h2>
|
| 225 |
+
</div>
|
| 226 |
+
<p class="panel-note">These values become the API defaults when the caller omits form fields.</p>
|
| 227 |
+
</div>
|
| 228 |
+
<form method="post" action="/admin/settings" class="settings-grid">
|
| 229 |
+
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
| 230 |
+
{% for field in settings_spec %}
|
| 231 |
+
<label class="field">
|
| 232 |
+
<span>{{ field.label }}</span>
|
| 233 |
+
{% if field.type == "select" %}
|
| 234 |
+
<select name="{{ field.name }}">
|
| 235 |
+
{% for option in field.options %}
|
| 236 |
+
<option value="{{ option }}" {% if settings[field.name] == option %}selected{% endif %}>{{ option }}</option>
|
| 237 |
+
{% endfor %}
|
| 238 |
+
</select>
|
| 239 |
+
{% else %}
|
| 240 |
+
<input type="text" name="{{ field.name }}" value="{{ settings[field.name] }}" />
|
| 241 |
+
{% endif %}
|
| 242 |
+
<small>{{ field.help }}</small>
|
| 243 |
+
</label>
|
| 244 |
+
{% endfor %}
|
| 245 |
+
<button type="submit" class="button button--primary">Save runtime settings</button>
|
| 246 |
+
</form>
|
| 247 |
+
</section>
|
| 248 |
+
|
| 249 |
+
<section class="panel">
|
| 250 |
+
<div class="panel-heading">
|
| 251 |
+
<div>
|
| 252 |
+
<p class="eyebrow">Access control</p>
|
| 253 |
+
<h2>API keys</h2>
|
| 254 |
+
</div>
|
| 255 |
+
<p class="panel-note">Clients need either <code>X-API-Key</code> or <code>Authorization: Bearer</code>.</p>
|
| 256 |
+
</div>
|
| 257 |
+
<form method="post" action="/admin/api-keys" class="inline-form">
|
| 258 |
+
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
| 259 |
+
<input type="text" name="name" placeholder="Key label, e.g. production-bot" required />
|
| 260 |
+
<button type="submit" class="button button--primary">Create key</button>
|
| 261 |
+
</form>
|
| 262 |
+
<div class="table-wrap">
|
| 263 |
+
<table class="data-table">
|
| 264 |
+
<thead>
|
| 265 |
+
<tr>
|
| 266 |
+
<th>Name</th>
|
| 267 |
+
<th>Prefix</th>
|
| 268 |
+
<th>State</th>
|
| 269 |
+
<th>Created</th>
|
| 270 |
+
<th>Last used</th>
|
| 271 |
+
<th>Actions</th>
|
| 272 |
+
</tr>
|
| 273 |
+
</thead>
|
| 274 |
+
<tbody>
|
| 275 |
+
{% for api_key in api_keys %}
|
| 276 |
+
<tr>
|
| 277 |
+
<td>{{ api_key.name }}</td>
|
| 278 |
+
<td class="mono">{{ api_key.key_prefix }}...</td>
|
| 279 |
+
<td>
|
| 280 |
+
<span class="status-pill status-pill--{% if api_key.enabled %}succeeded{% else %}failed{% endif %}">
|
| 281 |
+
{% if api_key.enabled %}enabled{% else %}disabled{% endif %}
|
| 282 |
+
</span>
|
| 283 |
+
</td>
|
| 284 |
+
<td class="mono">{{ api_key.created_at[:19].replace("T", " ") }}</td>
|
| 285 |
+
<td class="mono">{{ api_key.last_used_at[:19].replace("T", " ") if api_key.last_used_at else "-" }}</td>
|
| 286 |
+
<td>
|
| 287 |
+
<div class="action-row">
|
| 288 |
+
<form method="post" action="/admin/api-keys/{{ api_key.id }}/toggle">
|
| 289 |
+
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
| 290 |
+
<input type="hidden" name="enabled" value="{% if api_key.enabled %}0{% else %}1{% endif %}" />
|
| 291 |
+
<button type="submit" class="button button--ghost">
|
| 292 |
+
{% if api_key.enabled %}Disable{% else %}Enable{% endif %}
|
| 293 |
+
</button>
|
| 294 |
+
</form>
|
| 295 |
+
<form method="post" action="/admin/api-keys/{{ api_key.id }}/delete">
|
| 296 |
+
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
| 297 |
+
<button type="submit" class="button button--danger">Delete</button>
|
| 298 |
+
</form>
|
| 299 |
+
</div>
|
| 300 |
+
</td>
|
| 301 |
+
</tr>
|
| 302 |
+
{% else %}
|
| 303 |
+
<tr>
|
| 304 |
+
<td colspan="6" class="empty-state">No API keys yet.</td>
|
| 305 |
+
</tr>
|
| 306 |
+
{% endfor %}
|
| 307 |
+
</tbody>
|
| 308 |
+
</table>
|
| 309 |
+
</div>
|
| 310 |
+
</section>
|
| 311 |
+
|
| 312 |
+
<section class="panel">
|
| 313 |
+
<div class="panel-heading">
|
| 314 |
+
<div>
|
| 315 |
+
<p class="eyebrow">Security</p>
|
| 316 |
+
<h2>Admin password</h2>
|
| 317 |
+
</div>
|
| 318 |
+
<p class="panel-note">Rotate this after first deployment if you used a temporary secret.</p>
|
| 319 |
+
</div>
|
| 320 |
+
<form method="post" action="/admin/password" class="stack-form">
|
| 321 |
+
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
| 322 |
+
<label>
|
| 323 |
+
<span>Current password</span>
|
| 324 |
+
<input type="password" name="current_password" autocomplete="current-password" required />
|
| 325 |
+
</label>
|
| 326 |
+
<label>
|
| 327 |
+
<span>New password</span>
|
| 328 |
+
<input type="password" name="new_password" autocomplete="new-password" minlength="10" required />
|
| 329 |
+
</label>
|
| 330 |
+
<label>
|
| 331 |
+
<span>Confirm new password</span>
|
| 332 |
+
<input type="password" name="confirm_password" autocomplete="new-password" minlength="10" required />
|
| 333 |
+
</label>
|
| 334 |
+
<button type="submit" class="button button--secondary">Update admin password</button>
|
| 335 |
+
</form>
|
| 336 |
+
</section>
|
| 337 |
+
|
| 338 |
+
<section class="panel">
|
| 339 |
+
<div class="panel-heading">
|
| 340 |
+
<div>
|
| 341 |
+
<p class="eyebrow">API usage</p>
|
| 342 |
+
<h2>Client example</h2>
|
| 343 |
+
</div>
|
| 344 |
+
<p class="panel-note">Replace the placeholder key and image path before use.</p>
|
| 345 |
+
</div>
|
| 346 |
+
<pre class="code-block"><code>curl -X POST "$SPACE_URL/api/v1/upscale" \
|
| 347 |
+
-H "X-API-Key: rcg_replace_me" \
|
| 348 |
+
-F "file=@input.png" \
|
| 349 |
+
-F "scale={{ settings.default_scale }}" \
|
| 350 |
+
-F "variant={{ settings.default_variant }}" \
|
| 351 |
+
-F "alpha={{ settings.default_alpha }}" \
|
| 352 |
+
-F "response_format=png" \
|
| 353 |
+
-o output.png</code></pre>
|
| 354 |
+
</section>
|
| 355 |
+
</section>
|
| 356 |
+
</main>
|
| 357 |
+
{% endblock %}
|
app/templates/login.html
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}{{ app_name }} | Admin{% endblock %}
|
| 4 |
+
{% block body_class %}auth-shell{% endblock %}
|
| 5 |
+
|
| 6 |
+
{% block body %}
|
| 7 |
+
<main class="auth-layout">
|
| 8 |
+
<section class="auth-panel auth-panel--hero">
|
| 9 |
+
<p class="eyebrow">Real-CUGAN control plane</p>
|
| 10 |
+
<h1>Private anime upscaling, tuned for slow CPU and long-running jobs.</h1>
|
| 11 |
+
<p class="auth-copy">
|
| 12 |
+
This panel controls API keys, runtime limits, model availability and recent job health.
|
| 13 |
+
It is designed for Hugging Face CPU Basic, where stability matters more than throughput.
|
| 14 |
+
</p>
|
| 15 |
+
<div class="auth-grid">
|
| 16 |
+
<article>
|
| 17 |
+
<span class="metric-label">Deployment target</span>
|
| 18 |
+
<strong>Hugging Face Docker Space</strong>
|
| 19 |
+
<p>Single-process FastAPI, session auth, SQLite state.</p>
|
| 20 |
+
</article>
|
| 21 |
+
<article>
|
| 22 |
+
<span class="metric-label">Inference mode</span>
|
| 23 |
+
<strong>Real-CUGAN on CPU</strong>
|
| 24 |
+
<p>Whole-image first, tile fallback when memory pressure appears.</p>
|
| 25 |
+
</article>
|
| 26 |
+
<article>
|
| 27 |
+
<span class="metric-label">Traffic posture</span>
|
| 28 |
+
<strong>Serial requests</strong>
|
| 29 |
+
<p>One active upscale at a time, API key gate on every job.</p>
|
| 30 |
+
</article>
|
| 31 |
+
</div>
|
| 32 |
+
</section>
|
| 33 |
+
|
| 34 |
+
<section class="auth-panel auth-panel--form">
|
| 35 |
+
{% if flash %}
|
| 36 |
+
<div class="flash flash--{{ flash.level }}">{{ flash.message }}</div>
|
| 37 |
+
{% endif %}
|
| 38 |
+
|
| 39 |
+
{% if admin_configured %}
|
| 40 |
+
<div class="panel-heading">
|
| 41 |
+
<p class="eyebrow">Admin access</p>
|
| 42 |
+
<h2>Sign in</h2>
|
| 43 |
+
<p>Use the local admin account for the control plane.</p>
|
| 44 |
+
</div>
|
| 45 |
+
<form method="post" action="/admin/login" class="stack-form">
|
| 46 |
+
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
| 47 |
+
<label>
|
| 48 |
+
<span>Username</span>
|
| 49 |
+
<input type="text" name="username" value="{{ admin_username }}" autocomplete="username" required />
|
| 50 |
+
</label>
|
| 51 |
+
<label>
|
| 52 |
+
<span>Password</span>
|
| 53 |
+
<input type="password" name="password" autocomplete="current-password" required />
|
| 54 |
+
</label>
|
| 55 |
+
<button type="submit" class="button button--primary">Sign in</button>
|
| 56 |
+
</form>
|
| 57 |
+
{% else %}
|
| 58 |
+
<div class="panel-heading">
|
| 59 |
+
<p class="eyebrow">First boot</p>
|
| 60 |
+
<h2>Initialize the admin account</h2>
|
| 61 |
+
<p>
|
| 62 |
+
No admin user exists yet. Set the first password here, or provide
|
| 63 |
+
<code>ADMIN_PASSWORD</code> as a Space secret before launch.
|
| 64 |
+
</p>
|
| 65 |
+
</div>
|
| 66 |
+
<form method="post" action="/admin/bootstrap" class="stack-form">
|
| 67 |
+
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
| 68 |
+
<label>
|
| 69 |
+
<span>Admin username</span>
|
| 70 |
+
<input type="text" value="{{ admin_username }}" disabled />
|
| 71 |
+
</label>
|
| 72 |
+
<label>
|
| 73 |
+
<span>New password</span>
|
| 74 |
+
<input type="password" name="password" autocomplete="new-password" minlength="10" required />
|
| 75 |
+
</label>
|
| 76 |
+
<label>
|
| 77 |
+
<span>Confirm password</span>
|
| 78 |
+
<input type="password" name="confirm_password" autocomplete="new-password" minlength="10" required />
|
| 79 |
+
</label>
|
| 80 |
+
<button type="submit" class="button button--primary">Create admin account</button>
|
| 81 |
+
</form>
|
| 82 |
+
{% endif %}
|
| 83 |
+
</section>
|
| 84 |
+
</main>
|
| 85 |
+
{% endblock %}
|
app/vendor/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Vendor bootstrap package."""
|
app/vendor/upcunet_v3.py
ADDED
|
@@ -0,0 +1,1334 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''
|
| 2 |
+
cache_mode:
|
| 3 |
+
0:使用cache缓存必要参数
|
| 4 |
+
1:使用cache缓存必要参数,对cache进行8bit量化节省显存,带来小许延时增长
|
| 5 |
+
2:不使用cache,耗时约为mode0的2倍,但是显存不受输入图像分辨率限制,tile_mode填得够大,1.5G显存可超任意比例
|
| 6 |
+
'''
|
| 7 |
+
import torch,pdb
|
| 8 |
+
from torch import nn as nn
|
| 9 |
+
from torch.nn import functional as F
|
| 10 |
+
import os,sys
|
| 11 |
+
import numpy as np
|
| 12 |
+
root_path=os.path.abspath('.')
|
| 13 |
+
sys.path.append(root_path)
|
| 14 |
+
def q(inp,cache_mode):
|
| 15 |
+
maxx = inp.max()
|
| 16 |
+
minn = inp.min()
|
| 17 |
+
delta = maxx - minn
|
| 18 |
+
if(cache_mode==2):
|
| 19 |
+
return ((inp-minn)/delta*255).round().byte().cpu(),delta,minn,inp.device#大概3倍延时#太慢了,屏蔽该模式
|
| 20 |
+
elif(cache_mode==1):
|
| 21 |
+
return ((inp-minn)/delta*255).round().byte(),delta,minn,inp.device#不用CPU转移
|
| 22 |
+
def dq(inp,if_half,cache_mode,delta,minn,device):
|
| 23 |
+
if(cache_mode==2):
|
| 24 |
+
if(if_half==True):return inp.to(device).half()/255*delta+minn
|
| 25 |
+
else:return inp.to(device).float()/255*delta+minn
|
| 26 |
+
elif(cache_mode==1):
|
| 27 |
+
if(if_half==True):return inp.half()/255*delta+minn#不用CPU转移
|
| 28 |
+
else:return inp.float()/255*delta+minn
|
| 29 |
+
class SEBlock(nn.Module):
|
| 30 |
+
def __init__(self, in_channels, reduction=8, bias=False):
|
| 31 |
+
super(SEBlock, self).__init__()
|
| 32 |
+
self.conv1 = nn.Conv2d(in_channels, in_channels // reduction, 1, 1, 0, bias=bias)
|
| 33 |
+
self.conv2 = nn.Conv2d(in_channels // reduction, in_channels, 1, 1, 0, bias=bias)
|
| 34 |
+
|
| 35 |
+
def forward(self, x):
|
| 36 |
+
if ("Half" in x.type()): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 37 |
+
x0 = torch.mean(x.float(), dim=(2, 3), keepdim=True).half()
|
| 38 |
+
else:
|
| 39 |
+
x0 = torch.mean(x, dim=(2, 3), keepdim=True)
|
| 40 |
+
x0 = self.conv1(x0)
|
| 41 |
+
x0 = F.relu(x0, inplace=True)
|
| 42 |
+
x0 = self.conv2(x0)
|
| 43 |
+
x0 = torch.sigmoid(x0)
|
| 44 |
+
x = torch.mul(x, x0)
|
| 45 |
+
return x
|
| 46 |
+
|
| 47 |
+
def forward_mean(self, x,x0):
|
| 48 |
+
x0 = self.conv1(x0)
|
| 49 |
+
x0 = F.relu(x0, inplace=True)
|
| 50 |
+
x0 = self.conv2(x0)
|
| 51 |
+
x0 = torch.sigmoid(x0)
|
| 52 |
+
x = torch.mul(x, x0)
|
| 53 |
+
return x
|
| 54 |
+
class UNetConv(nn.Module):
|
| 55 |
+
def __init__(self, in_channels, mid_channels, out_channels, se):
|
| 56 |
+
super(UNetConv, self).__init__()
|
| 57 |
+
self.conv = nn.Sequential(
|
| 58 |
+
nn.Conv2d(in_channels, mid_channels, 3, 1, 0),
|
| 59 |
+
nn.LeakyReLU(0.1, inplace=True),
|
| 60 |
+
nn.Conv2d(mid_channels, out_channels, 3, 1, 0),
|
| 61 |
+
nn.LeakyReLU(0.1, inplace=True),
|
| 62 |
+
)
|
| 63 |
+
if se:
|
| 64 |
+
self.seblock = SEBlock(out_channels, reduction=8, bias=True)
|
| 65 |
+
else:
|
| 66 |
+
self.seblock = None
|
| 67 |
+
|
| 68 |
+
def forward(self, x):
|
| 69 |
+
z = self.conv(x)
|
| 70 |
+
if self.seblock is not None:
|
| 71 |
+
z = self.seblock(z)
|
| 72 |
+
return z
|
| 73 |
+
class UNet1(nn.Module):
|
| 74 |
+
def __init__(self, in_channels, out_channels, deconv):
|
| 75 |
+
super(UNet1, self).__init__()
|
| 76 |
+
self.conv1 = UNetConv(in_channels, 32, 64, se=False)
|
| 77 |
+
self.conv1_down = nn.Conv2d(64, 64, 2, 2, 0)
|
| 78 |
+
self.conv2 = UNetConv(64, 128, 64, se=True)
|
| 79 |
+
self.conv2_up = nn.ConvTranspose2d(64, 64, 2, 2, 0)
|
| 80 |
+
self.conv3 = nn.Conv2d(64, 64, 3, 1, 0)
|
| 81 |
+
|
| 82 |
+
if deconv:
|
| 83 |
+
self.conv_bottom = nn.ConvTranspose2d(64, out_channels, 4, 2, 3)
|
| 84 |
+
else:
|
| 85 |
+
self.conv_bottom = nn.Conv2d(64, out_channels, 3, 1, 0)
|
| 86 |
+
|
| 87 |
+
for m in self.modules():
|
| 88 |
+
if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
|
| 89 |
+
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
|
| 90 |
+
elif isinstance(m, nn.Linear):
|
| 91 |
+
nn.init.normal_(m.weight, 0, 0.01)
|
| 92 |
+
if m.bias is not None:
|
| 93 |
+
nn.init.constant_(m.bias, 0)
|
| 94 |
+
|
| 95 |
+
def forward(self, x):
|
| 96 |
+
x1 = self.conv1(x)
|
| 97 |
+
x2 = self.conv1_down(x1)
|
| 98 |
+
x1 = F.pad(x1, (-4, -4, -4, -4))
|
| 99 |
+
x2 = F.leaky_relu(x2, 0.1, inplace=True)
|
| 100 |
+
x2 = self.conv2(x2)
|
| 101 |
+
x2 = self.conv2_up(x2)
|
| 102 |
+
x2 = F.leaky_relu(x2, 0.1, inplace=True)
|
| 103 |
+
x3 = self.conv3(x1 + x2)
|
| 104 |
+
x3 = F.leaky_relu(x3, 0.1, inplace=True)
|
| 105 |
+
z = self.conv_bottom(x3)
|
| 106 |
+
return z
|
| 107 |
+
|
| 108 |
+
def forward_a(self, x):
|
| 109 |
+
x1 = self.conv1(x)
|
| 110 |
+
x2 = self.conv1_down(x1)
|
| 111 |
+
x1 = F.pad(x1, (-4, -4, -4, -4))
|
| 112 |
+
x2 = F.leaky_relu(x2, 0.1, inplace=True)
|
| 113 |
+
x2 = self.conv2.conv(x2)
|
| 114 |
+
return x1,x2
|
| 115 |
+
|
| 116 |
+
def forward_b(self, x1,x2):
|
| 117 |
+
x2 = self.conv2_up(x2)
|
| 118 |
+
x2 = F.leaky_relu(x2, 0.1, inplace=True)
|
| 119 |
+
x3 = self.conv3(x1 + x2)
|
| 120 |
+
x3 = F.leaky_relu(x3, 0.1, inplace=True)
|
| 121 |
+
z = self.conv_bottom(x3)
|
| 122 |
+
return z
|
| 123 |
+
class UNet1x3(nn.Module):
|
| 124 |
+
def __init__(self, in_channels, out_channels, deconv):
|
| 125 |
+
super(UNet1x3, self).__init__()
|
| 126 |
+
self.conv1 = UNetConv(in_channels, 32, 64, se=False)
|
| 127 |
+
self.conv1_down = nn.Conv2d(64, 64, 2, 2, 0)
|
| 128 |
+
self.conv2 = UNetConv(64, 128, 64, se=True)
|
| 129 |
+
self.conv2_up = nn.ConvTranspose2d(64, 64, 2, 2, 0)
|
| 130 |
+
self.conv3 = nn.Conv2d(64, 64, 3, 1, 0)
|
| 131 |
+
|
| 132 |
+
if deconv:
|
| 133 |
+
self.conv_bottom = nn.ConvTranspose2d(64, out_channels, 5, 3, 2)
|
| 134 |
+
else:
|
| 135 |
+
self.conv_bottom = nn.Conv2d(64, out_channels, 3, 1, 0)
|
| 136 |
+
|
| 137 |
+
for m in self.modules():
|
| 138 |
+
if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
|
| 139 |
+
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
|
| 140 |
+
elif isinstance(m, nn.Linear):
|
| 141 |
+
nn.init.normal_(m.weight, 0, 0.01)
|
| 142 |
+
if m.bias is not None:
|
| 143 |
+
nn.init.constant_(m.bias, 0)
|
| 144 |
+
|
| 145 |
+
def forward(self, x):
|
| 146 |
+
x1 = self.conv1(x)
|
| 147 |
+
x2 = self.conv1_down(x1)
|
| 148 |
+
x1 = F.pad(x1, (-4, -4, -4, -4))
|
| 149 |
+
x2 = F.leaky_relu(x2, 0.1, inplace=True)
|
| 150 |
+
x2 = self.conv2(x2)
|
| 151 |
+
x2 = self.conv2_up(x2)
|
| 152 |
+
x2 = F.leaky_relu(x2, 0.1, inplace=True)
|
| 153 |
+
x3 = self.conv3(x1 + x2)
|
| 154 |
+
x3 = F.leaky_relu(x3, 0.1, inplace=True)
|
| 155 |
+
z = self.conv_bottom(x3)
|
| 156 |
+
return z
|
| 157 |
+
|
| 158 |
+
def forward_a(self, x):
|
| 159 |
+
x1 = self.conv1(x)
|
| 160 |
+
x2 = self.conv1_down(x1)
|
| 161 |
+
x1 = F.pad(x1, (-4, -4, -4, -4))
|
| 162 |
+
x2 = F.leaky_relu(x2, 0.1, inplace=True)
|
| 163 |
+
x2 = self.conv2.conv(x2)
|
| 164 |
+
return x1,x2
|
| 165 |
+
|
| 166 |
+
def forward_b(self, x1,x2):
|
| 167 |
+
x2 = self.conv2_up(x2)
|
| 168 |
+
x2 = F.leaky_relu(x2, 0.1, inplace=True)
|
| 169 |
+
x3 = self.conv3(x1 + x2)
|
| 170 |
+
x3 = F.leaky_relu(x3, 0.1, inplace=True)
|
| 171 |
+
z = self.conv_bottom(x3)
|
| 172 |
+
return z
|
| 173 |
+
class UNet2(nn.Module):
|
| 174 |
+
def __init__(self, in_channels, out_channels, deconv):
|
| 175 |
+
super(UNet2, self).__init__()
|
| 176 |
+
|
| 177 |
+
self.conv1 = UNetConv(in_channels, 32, 64, se=False)
|
| 178 |
+
self.conv1_down = nn.Conv2d(64, 64, 2, 2, 0)
|
| 179 |
+
self.conv2 = UNetConv(64, 64, 128, se=True)
|
| 180 |
+
self.conv2_down = nn.Conv2d(128, 128, 2, 2, 0)
|
| 181 |
+
self.conv3 = UNetConv(128, 256, 128, se=True)
|
| 182 |
+
self.conv3_up = nn.ConvTranspose2d(128, 128, 2, 2, 0)
|
| 183 |
+
self.conv4 = UNetConv(128, 64, 64, se=True)
|
| 184 |
+
self.conv4_up = nn.ConvTranspose2d(64, 64, 2, 2, 0)
|
| 185 |
+
self.conv5 = nn.Conv2d(64, 64, 3, 1, 0)
|
| 186 |
+
|
| 187 |
+
if deconv:
|
| 188 |
+
self.conv_bottom = nn.ConvTranspose2d(64, out_channels, 4, 2, 3)
|
| 189 |
+
else:
|
| 190 |
+
self.conv_bottom = nn.Conv2d(64, out_channels, 3, 1, 0)
|
| 191 |
+
|
| 192 |
+
for m in self.modules():
|
| 193 |
+
if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
|
| 194 |
+
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
|
| 195 |
+
elif isinstance(m, nn.Linear):
|
| 196 |
+
nn.init.normal_(m.weight, 0, 0.01)
|
| 197 |
+
if m.bias is not None:
|
| 198 |
+
nn.init.constant_(m.bias, 0)
|
| 199 |
+
|
| 200 |
+
def forward(self, x,alpha=1):
|
| 201 |
+
x1 = self.conv1(x)
|
| 202 |
+
x2 = self.conv1_down(x1)
|
| 203 |
+
x1 = F.pad(x1, (-16, -16, -16, -16))
|
| 204 |
+
x2 = F.leaky_relu(x2, 0.1, inplace=True)
|
| 205 |
+
x2 = self.conv2(x2)
|
| 206 |
+
x3 = self.conv2_down(x2)
|
| 207 |
+
x2 = F.pad(x2, (-4, -4, -4, -4))
|
| 208 |
+
x3 = F.leaky_relu(x3, 0.1, inplace=True)
|
| 209 |
+
x3 = self.conv3(x3)
|
| 210 |
+
x3 = self.conv3_up(x3)
|
| 211 |
+
x3 = F.leaky_relu(x3, 0.1, inplace=True)
|
| 212 |
+
x4 = self.conv4(x2 + x3)
|
| 213 |
+
x4*=alpha
|
| 214 |
+
x4 = self.conv4_up(x4)
|
| 215 |
+
x4 = F.leaky_relu(x4, 0.1, inplace=True)
|
| 216 |
+
x5 = self.conv5(x1 + x4)
|
| 217 |
+
x5 = F.leaky_relu(x5, 0.1, inplace=True)
|
| 218 |
+
z = self.conv_bottom(x5)
|
| 219 |
+
return z
|
| 220 |
+
|
| 221 |
+
def forward_a(self, x):#conv234结尾有se
|
| 222 |
+
x1 = self.conv1(x)
|
| 223 |
+
x2 = self.conv1_down(x1)
|
| 224 |
+
x1 = F.pad(x1, (-16, -16, -16, -16))
|
| 225 |
+
x2 = F.leaky_relu(x2, 0.1, inplace=True)
|
| 226 |
+
x2 = self.conv2.conv(x2)
|
| 227 |
+
return x1,x2
|
| 228 |
+
|
| 229 |
+
def forward_b(self, x2): # conv234结尾有se
|
| 230 |
+
x3 = self.conv2_down(x2)
|
| 231 |
+
x2 = F.pad(x2, (-4, -4, -4, -4))
|
| 232 |
+
x3 = F.leaky_relu(x3, 0.1, inplace=True)
|
| 233 |
+
x3 = self.conv3.conv(x3)
|
| 234 |
+
return x2,x3
|
| 235 |
+
|
| 236 |
+
def forward_c(self, x2,x3): # conv234结尾有se
|
| 237 |
+
x3 = self.conv3_up(x3)
|
| 238 |
+
x3 = F.leaky_relu(x3, 0.1, inplace=True)
|
| 239 |
+
x4 = self.conv4.conv(x2 + x3)
|
| 240 |
+
return x4
|
| 241 |
+
|
| 242 |
+
def forward_d(self, x1,x4): # conv234结尾有se
|
| 243 |
+
x4 = self.conv4_up(x4)
|
| 244 |
+
x4 = F.leaky_relu(x4, 0.1, inplace=True)
|
| 245 |
+
x5 = self.conv5(x1 + x4)
|
| 246 |
+
x5 = F.leaky_relu(x5, 0.1, inplace=True)
|
| 247 |
+
|
| 248 |
+
z = self.conv_bottom(x5)
|
| 249 |
+
return z
|
| 250 |
+
class UpCunet2x(nn.Module):
|
| 251 |
+
def __init__(self, in_channels=3, out_channels=3):
|
| 252 |
+
super(UpCunet2x, self).__init__()
|
| 253 |
+
self.unet1 = UNet1(in_channels, out_channels, deconv=True)
|
| 254 |
+
self.unet2 = UNet2(in_channels, out_channels, deconv=False)
|
| 255 |
+
def forward(self, x,tile_mode,cache_mode,alpha,pro):
|
| 256 |
+
n, c, h0, w0 = x.shape
|
| 257 |
+
if ("Half" in x.type()):if_half=True
|
| 258 |
+
else:if_half=False
|
| 259 |
+
if(tile_mode==0):#���tile
|
| 260 |
+
ph = ((h0 - 1) // 2 + 1) * 2
|
| 261 |
+
pw = ((w0 - 1) // 2 + 1) * 2
|
| 262 |
+
x = F.pad(x, (18, 18 + pw - w0, 18, 18 + ph - h0), 'reflect') # 需要保证被2整除
|
| 263 |
+
x = self.unet1.forward(x)
|
| 264 |
+
x0 = self.unet2.forward(x,alpha)
|
| 265 |
+
x = F.pad(x, (-20, -20, -20, -20))
|
| 266 |
+
x = torch.add(x0, x)
|
| 267 |
+
if (w0 != pw or h0 != ph): x = x[:, :, :h0 * 2, :w0 * 2]
|
| 268 |
+
if(pro):
|
| 269 |
+
return ((x-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 270 |
+
else:
|
| 271 |
+
return (x * 255).round().clamp_(0, 255).byte()
|
| 272 |
+
elif(tile_mode==1):# 对长边减半
|
| 273 |
+
if(w0>=h0):
|
| 274 |
+
crop_size_w=((w0-1)//4*4+4)//2#减半后能被2整除,所以要先被4整除
|
| 275 |
+
crop_size_h=(h0-1)//2*2+2#能被2整除
|
| 276 |
+
else:
|
| 277 |
+
crop_size_h=((h0-1)//4*4+4)//2#减半后能被2整除,所以要先被4整除
|
| 278 |
+
crop_size_w=(w0-1)//2*2+2#能被2整除
|
| 279 |
+
crop_size=(crop_size_h,crop_size_w)
|
| 280 |
+
elif(tile_mode>=2):
|
| 281 |
+
tile_mode=min(min(h0,w0)//128,int(tile_mode))#最小短边为128*128
|
| 282 |
+
t2=tile_mode*2
|
| 283 |
+
crop_size=(((h0-1)//t2*t2+t2)//tile_mode,((w0-1)//t2*t2+t2)//tile_mode)
|
| 284 |
+
else:
|
| 285 |
+
print("tile_mode config error")
|
| 286 |
+
os._exit(233)
|
| 287 |
+
|
| 288 |
+
ph = ((h0 - 1) // crop_size[0] + 1) * crop_size[0]
|
| 289 |
+
pw = ((w0 - 1) // crop_size[1] + 1) * crop_size[1]
|
| 290 |
+
x=F.pad(x,(18,18+pw-w0,18,18+ph-h0),'reflect')
|
| 291 |
+
n,c,h,w=x.shape
|
| 292 |
+
if (if_half):se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 293 |
+
else:se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 294 |
+
n_patch=0
|
| 295 |
+
tmp_dict={}
|
| 296 |
+
for i in range(0,h-36,crop_size[0]):
|
| 297 |
+
tmp_dict[i]={}
|
| 298 |
+
for j in range(0,w-36,crop_size[1]):
|
| 299 |
+
x_crop=x[:,:,i:i+crop_size[0]+36,j:j+crop_size[1]+36]
|
| 300 |
+
n,c1,h1,w1=x_crop.shape
|
| 301 |
+
tmp0,x_crop = self.unet1.forward_a(x_crop)
|
| 302 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 303 |
+
tmp_se_mean = torch.mean(x_crop.float(), dim=(2, 3),keepdim=True).half()
|
| 304 |
+
else:
|
| 305 |
+
tmp_se_mean = torch.mean(x_crop, dim=(2, 3),keepdim=True)
|
| 306 |
+
se_mean0+=tmp_se_mean
|
| 307 |
+
n_patch+=1
|
| 308 |
+
tmp_dict[i][j]=(tmp0,x_crop)
|
| 309 |
+
se_mean0/=n_patch
|
| 310 |
+
if (if_half):se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 311 |
+
else:se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 312 |
+
for i in range(0,h-36,crop_size[0]):
|
| 313 |
+
for j in range(0,w-36,crop_size[1]):
|
| 314 |
+
tmp0, x_crop=tmp_dict[i][j]
|
| 315 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 316 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 317 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 318 |
+
opt_unet1 = F.pad(opt_unet1,(-20,-20,-20,-20))
|
| 319 |
+
if(cache_mode):opt_unet1,tmp_x1=q(opt_unet1,cache_mode), q(tmp_x1,cache_mode)
|
| 320 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 321 |
+
tmp_se_mean = torch.mean(tmp_x2.float(), dim=(2, 3),keepdim=True).half()
|
| 322 |
+
else:
|
| 323 |
+
tmp_se_mean = torch.mean(tmp_x2, dim=(2, 3),keepdim=True)
|
| 324 |
+
if(cache_mode):tmp_x2=q(tmp_x2,cache_mode)
|
| 325 |
+
se_mean1+=tmp_se_mean
|
| 326 |
+
tmp_dict[i][j]=(opt_unet1,tmp_x1,tmp_x2)
|
| 327 |
+
se_mean1/=n_patch
|
| 328 |
+
if (if_half):se_mean0=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 329 |
+
else:se_mean0=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 330 |
+
for i in range(0,h-36,crop_size[0]):
|
| 331 |
+
for j in range(0,w-36,crop_size[1]):
|
| 332 |
+
opt_unet1,tmp_x1, tmp_x2=tmp_dict[i][j]
|
| 333 |
+
if(cache_mode):tmp_x2=dq(tmp_x2[0],if_half,cache_mode,tmp_x2[1],tmp_x2[2],tmp_x2[3])
|
| 334 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1)
|
| 335 |
+
tmp_x2,tmp_x3=self.unet2.forward_b(tmp_x2)
|
| 336 |
+
if(cache_mode):tmp_x2=q(tmp_x2,cache_mode)
|
| 337 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 338 |
+
tmp_se_mean = torch.mean(tmp_x3.float(), dim=(2, 3),keepdim=True).half()
|
| 339 |
+
else:
|
| 340 |
+
tmp_se_mean = torch.mean(tmp_x3, dim=(2, 3),keepdim=True)
|
| 341 |
+
if(cache_mode):tmp_x3=q(tmp_x3,cache_mode)
|
| 342 |
+
se_mean0+=tmp_se_mean
|
| 343 |
+
tmp_dict[i][j]=(opt_unet1,tmp_x1,tmp_x2,tmp_x3)
|
| 344 |
+
se_mean0/=n_patch
|
| 345 |
+
if (if_half):se_mean1=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 346 |
+
else:se_mean1=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 347 |
+
for i in range(0,h-36,crop_size[0]):
|
| 348 |
+
for j in range(0,w-36,crop_size[1]):
|
| 349 |
+
opt_unet1,tmp_x1, tmp_x2,tmp_x3=tmp_dict[i][j]
|
| 350 |
+
if(cache_mode):tmp_x3=dq(tmp_x3[0],if_half,cache_mode,tmp_x3[1],tmp_x3[2],tmp_x3[3])
|
| 351 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean0)
|
| 352 |
+
if(cache_mode):tmp_x2=dq(tmp_x2[0],if_half,cache_mode,tmp_x2[1],tmp_x2[2],tmp_x2[3])
|
| 353 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)*alpha
|
| 354 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 355 |
+
tmp_se_mean = torch.mean(tmp_x4.float(), dim=(2, 3),keepdim=True).half()
|
| 356 |
+
else:
|
| 357 |
+
tmp_se_mean = torch.mean(tmp_x4, dim=(2, 3),keepdim=True)
|
| 358 |
+
if(cache_mode):tmp_x4=q(tmp_x4,cache_mode)
|
| 359 |
+
se_mean1+=tmp_se_mean
|
| 360 |
+
tmp_dict[i][j]=(opt_unet1,tmp_x1,tmp_x4)
|
| 361 |
+
se_mean1/=n_patch
|
| 362 |
+
res = torch.zeros((n, c, h * 2 - 72, w * 2 - 72),dtype=torch.uint8,device=x.device)
|
| 363 |
+
for i in range(0,h-36,crop_size[0]):
|
| 364 |
+
for j in range(0,w-36,crop_size[1]):
|
| 365 |
+
x,tmp_x1, tmp_x4=tmp_dict[i][j]
|
| 366 |
+
if(cache_mode):tmp_x4=dq(tmp_x4[0],if_half,cache_mode,tmp_x4[1],tmp_x4[2],tmp_x4[3])
|
| 367 |
+
tmp_x4=self.unet2.conv4.seblock.forward_mean(tmp_x4,se_mean1)
|
| 368 |
+
if(cache_mode):tmp_x1=dq(tmp_x1[0],if_half,cache_mode,tmp_x1[1],tmp_x1[2],tmp_x1[3])
|
| 369 |
+
x0=self.unet2.forward_d(tmp_x1,tmp_x4)
|
| 370 |
+
if(cache_mode):x = dq(x[0], if_half, cache_mode,x[1], x[2], x[3])
|
| 371 |
+
del tmp_dict[i][j]
|
| 372 |
+
x = torch.add(x0, x)#x0是unet2的最终输出
|
| 373 |
+
if(pro):
|
| 374 |
+
res[:, :, i * 2:i * 2 + h1 * 2 - 72, j * 2:j * 2 + w1 * 2 - 72] = ((x-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 375 |
+
else:
|
| 376 |
+
res[:, :, i * 2:i * 2 + h1 * 2 - 72, j * 2:j * 2 + w1 * 2 - 72] = (x*255).round().clamp_(0, 255).byte()
|
| 377 |
+
del tmp_dict
|
| 378 |
+
#torch.cuda.empty_cache()
|
| 379 |
+
if(w0!=pw or h0!=ph):res=res[:,:,:h0*2,:w0*2]
|
| 380 |
+
return res
|
| 381 |
+
def forward_gap_sync(self, x,tile_mode,alpha,pro):
|
| 382 |
+
n, c, h0, w0 = x.shape
|
| 383 |
+
if("Half" in x.type()):if_half=True
|
| 384 |
+
else:if_half=False
|
| 385 |
+
if(tile_mode==0):#不tile
|
| 386 |
+
ph = ((h0 - 1) // 2 + 1) * 2
|
| 387 |
+
pw = ((w0 - 1) // 2 + 1) * 2
|
| 388 |
+
x = F.pad(x, (18, 18 + pw - w0, 18, 18 + ph - h0), 'reflect') # 需要保证被2整除
|
| 389 |
+
x = self.unet1.forward(x)
|
| 390 |
+
x0 = self.unet2.forward(x,alpha)
|
| 391 |
+
x = F.pad(x, (-20, -20, -20, -20))
|
| 392 |
+
x = torch.add(x0, x)
|
| 393 |
+
if (w0 != pw or h0 != ph): x = x[:, :, :h0 * 2, :w0 * 2]
|
| 394 |
+
if(pro):
|
| 395 |
+
return ((x-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 396 |
+
else:
|
| 397 |
+
return (x * 255).round().clamp_(0, 255).byte()
|
| 398 |
+
elif(tile_mode==1):# 对长边减半
|
| 399 |
+
if(w0>=h0):
|
| 400 |
+
crop_size_w=((w0-1)//4*4+4)//2#减半后能被2整除,所以要先被4整除
|
| 401 |
+
crop_size_h=(h0-1)//2*2+2#能被2整除
|
| 402 |
+
else:
|
| 403 |
+
crop_size_h=((h0-1)//4*4+4)//2#减半后能被2整除,所以要先被4整除
|
| 404 |
+
crop_size_w=(w0-1)//2*2+2#能被2整除
|
| 405 |
+
crop_size=(crop_size_h,crop_size_w)#6.6G
|
| 406 |
+
elif(tile_mode>=2):#hw都减半
|
| 407 |
+
tile_mode=min(min(h0,w0)//128,int(tile_mode))#最小短边为128*128
|
| 408 |
+
t2=tile_mode*2
|
| 409 |
+
crop_size=(((h0-1)//t2*t2+t2)//tile_mode,((w0-1)//t2*t2+t2)//tile_mode)
|
| 410 |
+
else:
|
| 411 |
+
print("tile_mode config error")
|
| 412 |
+
os._exit(233)
|
| 413 |
+
ph = ((h0 - 1) // crop_size[0] + 1) * crop_size[0]
|
| 414 |
+
pw = ((w0 - 1) // crop_size[1] + 1) * crop_size[1]
|
| 415 |
+
x=F.pad(x,(18,18+pw-w0,18,18+ph-h0),'reflect')
|
| 416 |
+
n,c,h,w=x.shape
|
| 417 |
+
if (if_half):se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 418 |
+
else:se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 419 |
+
n_patch=0
|
| 420 |
+
h1,w1=crop_size[0]+36,crop_size[1]+36
|
| 421 |
+
######stage1
|
| 422 |
+
for i in range(0,h-36,crop_size[0]):
|
| 423 |
+
for j in range(0,w-36,crop_size[1]):
|
| 424 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+36,j:j+crop_size[1]+36])
|
| 425 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 426 |
+
tmp_se_mean = torch.mean(x_crop.float(), dim=(2, 3),keepdim=True).half()
|
| 427 |
+
else:
|
| 428 |
+
tmp_se_mean = torch.mean(x_crop, dim=(2, 3),keepdim=True)
|
| 429 |
+
se_mean0+=tmp_se_mean
|
| 430 |
+
n_patch+=1
|
| 431 |
+
se_mean0/=n_patch
|
| 432 |
+
######stage1+state2
|
| 433 |
+
if (if_half):se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 434 |
+
else:se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 435 |
+
for i in range(0,h-36,crop_size[0]):
|
| 436 |
+
for j in range(0,w-36,crop_size[1]):
|
| 437 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+36,j:j+crop_size[1]+36])
|
| 438 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 439 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 440 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 441 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 442 |
+
tmp_se_mean = torch.mean(tmp_x2.float(), dim=(2, 3),keepdim=True).half()
|
| 443 |
+
else:
|
| 444 |
+
tmp_se_mean = torch.mean(tmp_x2, dim=(2, 3),keepdim=True)
|
| 445 |
+
se_mean1+=tmp_se_mean
|
| 446 |
+
se_mean1/=n_patch
|
| 447 |
+
######stage1+state2+state3
|
| 448 |
+
if (if_half):se_mean2=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 449 |
+
else:se_mean2=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 450 |
+
for i in range(0,h-36,crop_size[0]):
|
| 451 |
+
for j in range(0,w-36,crop_size[1]):
|
| 452 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+36,j:j+crop_size[1]+36])
|
| 453 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 454 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 455 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 456 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1)
|
| 457 |
+
tmp_x2,tmp_x3=self.unet2.forward_b(tmp_x2)
|
| 458 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 459 |
+
tmp_se_mean = torch.mean(tmp_x3.float(), dim=(2, 3),keepdim=True).half()
|
| 460 |
+
else:
|
| 461 |
+
tmp_se_mean = torch.mean(tmp_x3, dim=(2, 3),keepdim=True)
|
| 462 |
+
se_mean2+=tmp_se_mean
|
| 463 |
+
se_mean2/=n_patch
|
| 464 |
+
#########stage1+state2+state3+stage4
|
| 465 |
+
if (if_half):se_mean3=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 466 |
+
else:se_mean3=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 467 |
+
if(if_half):
|
| 468 |
+
se_mean3=se_mean3.half()
|
| 469 |
+
for i in range(0,h-36,crop_size[0]):
|
| 470 |
+
for j in range(0,w-36,crop_size[1]):
|
| 471 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+36,j:j+crop_size[1]+36])
|
| 472 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 473 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 474 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 475 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1)
|
| 476 |
+
tmp_x2,tmp_x3=self.unet2.forward_b(tmp_x2)
|
| 477 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean2)
|
| 478 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)*alpha
|
| 479 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 480 |
+
tmp_se_mean = torch.mean(tmp_x4.float(), dim=(2, 3),keepdim=True).half()
|
| 481 |
+
else:
|
| 482 |
+
tmp_se_mean = torch.mean(tmp_x4, dim=(2, 3),keepdim=True)
|
| 483 |
+
se_mean3+=tmp_se_mean
|
| 484 |
+
se_mean3/=n_patch
|
| 485 |
+
###########stage1+state2+state3+stage4+stage_tail
|
| 486 |
+
res = torch.zeros((n, c, h * 2 - 72, w * 2 - 72),dtype=torch.uint8,device=x.device)
|
| 487 |
+
for i in range(0,h-36,crop_size[0]):
|
| 488 |
+
for j in range(0,w-36,crop_size[1]):
|
| 489 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+36,j:j+crop_size[1]+36])
|
| 490 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 491 |
+
x_crop=self.unet1.forward_b(tmp0,x_crop)
|
| 492 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(x_crop)
|
| 493 |
+
x_crop = F.pad(x_crop,(-20,-20,-20,-20))
|
| 494 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1)
|
| 495 |
+
tmp_x2, tmp_x3 = self.unet2.forward_b(tmp_x2)
|
| 496 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean2)
|
| 497 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)
|
| 498 |
+
tmp_x4=self.unet2.conv4.seblock.forward_mean(tmp_x4,se_mean3)
|
| 499 |
+
x0=self.unet2.forward_d(tmp_x1,tmp_x4)
|
| 500 |
+
x_crop = torch.add(x0, x_crop)
|
| 501 |
+
if(pro):
|
| 502 |
+
res[:, :, i * 2:i * 2 + h1 * 2 - 72, j * 2:j * 2 + w1 * 2 - 72] = ((x_crop-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 503 |
+
else:
|
| 504 |
+
res[:, :, i * 2:i * 2 + h1 * 2 - 72, j * 2:j * 2 + w1 * 2 - 72] = (x_crop* 255.0).round().clamp_(0, 255).byte()
|
| 505 |
+
#torch.cuda.empty_cache()
|
| 506 |
+
if(w0!=pw or h0!=ph):res=res[:,:,:h0*2,:w0*2]
|
| 507 |
+
return res
|
| 508 |
+
def forward_fast_rough(self, x,tile_mode,alpha,pro):
|
| 509 |
+
n, c, h0, w0 = x.shape
|
| 510 |
+
if ("Half" in x.type()):if_half=True
|
| 511 |
+
else:if_half=False
|
| 512 |
+
if(tile_mode<3):return self.forward(x,tile_mode,1,alpha,pro)#至少切成3x3
|
| 513 |
+
elif(tile_mode>=3):
|
| 514 |
+
tile_mode=min(min(h0,w0)//128,int(tile_mode))#最小短边为128*128
|
| 515 |
+
if (tile_mode < 3): return self.forward(x, tile_mode, 1, alpha,pro)
|
| 516 |
+
t2=tile_mode*2
|
| 517 |
+
crop_size=(((h0-1)//t2*t2+t2)//tile_mode,((w0-1)//t2*t2+t2)//tile_mode)
|
| 518 |
+
ph = ((h0 - 1) // crop_size[0] + 1) * crop_size[0]
|
| 519 |
+
pw = ((w0 - 1) // crop_size[1] + 1) * crop_size[1]
|
| 520 |
+
x=F.pad(x,(18,18+pw-w0,18,18+ph-h0),'reflect')
|
| 521 |
+
n,c,h,w=x.shape
|
| 522 |
+
h1,w1=crop_size[0]+36,crop_size[1]+36
|
| 523 |
+
n_patch=0
|
| 524 |
+
###########stage1+state2+state3+stage4
|
| 525 |
+
if (if_half):se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 526 |
+
else:se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 527 |
+
if (if_half):se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 528 |
+
else:se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 529 |
+
if (if_half):se_mean2=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 530 |
+
else:se_mean2=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 531 |
+
if (if_half):se_mean3=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 532 |
+
else:se_mean3=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 533 |
+
for i in range(0,h-36,crop_size[0]):
|
| 534 |
+
if((i//crop_size[0])%2==0):continue
|
| 535 |
+
for j in range(0,w-36,crop_size[1]):
|
| 536 |
+
if ((j//crop_size[1]) % 2 == 0): continue
|
| 537 |
+
n_patch+=1
|
| 538 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+36,j:j+crop_size[1]+36])
|
| 539 |
+
if(if_half):se_mean0 += torch.mean(x_crop.float(), dim=(2, 3),keepdim=True).half()
|
| 540 |
+
else:se_mean0 += torch.mean(x_crop, dim=(2, 3),keepdim=True)
|
| 541 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0/n_patch)
|
| 542 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 543 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 544 |
+
if(if_half):se_mean1 += torch.mean(tmp_x2.float(), dim=(2, 3),keepdim=True).half()
|
| 545 |
+
else:se_mean1 += torch.mean(tmp_x2, dim=(2, 3),keepdim=True)
|
| 546 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1/n_patch)
|
| 547 |
+
tmp_x2,tmp_x3=self.unet2.forward_b(tmp_x2)
|
| 548 |
+
if(if_half):se_mean2 += torch.mean(tmp_x3.float(), dim=(2, 3),keepdim=True).half()
|
| 549 |
+
else:se_mean2 += torch.mean(tmp_x3, dim=(2, 3),keepdim=True)
|
| 550 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean2/n_patch)
|
| 551 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)
|
| 552 |
+
if(if_half):se_mean3 += torch.mean(tmp_x4.float(), dim=(2, 3),keepdim=True).half()
|
| 553 |
+
else:se_mean3 += torch.mean(tmp_x4, dim=(2, 3),keepdim=True)
|
| 554 |
+
# print("2x-n_patch=%s,tile_mode=%s" % (n_patch,tile_mode))
|
| 555 |
+
###########stage1+state2+state3+stage4+stage_tail
|
| 556 |
+
res = torch.zeros((n, c, h * 2 - 72, w * 2 - 72),dtype=torch.uint8,device=x.device)
|
| 557 |
+
for i in range(0,h-36,crop_size[0]):
|
| 558 |
+
for j in range(0,w-36,crop_size[1]):
|
| 559 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+36,j:j+crop_size[1]+36])
|
| 560 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0/n_patch)
|
| 561 |
+
x_crop=self.unet1.forward_b(tmp0,x_crop)
|
| 562 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(x_crop)
|
| 563 |
+
x_crop = F.pad(x_crop,(-20,-20,-20,-20))
|
| 564 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1/n_patch)
|
| 565 |
+
tmp_x2, tmp_x3 = self.unet2.forward_b(tmp_x2)
|
| 566 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean2/n_patch)
|
| 567 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)
|
| 568 |
+
tmp_x4=self.unet2.conv4.seblock.forward_mean(tmp_x4,se_mean3/n_patch)
|
| 569 |
+
x0=self.unet2.forward_d(tmp_x1,tmp_x4)
|
| 570 |
+
x_crop = torch.add(x0, x_crop)
|
| 571 |
+
if(pro):
|
| 572 |
+
res[:, :, i * 2:i * 2 + h1 * 2 - 72, j * 2:j * 2 + w1 * 2 - 72] = ((x_crop-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 573 |
+
else:
|
| 574 |
+
res[:, :, i * 2:i * 2 + h1 * 2 - 72, j * 2:j * 2 + w1 * 2 - 72] = (x_crop* 255.0).round().clamp_(0, 255).byte()
|
| 575 |
+
#torch.cuda.empty_cache()
|
| 576 |
+
if(w0!=pw or h0!=ph):res=res[:,:,:h0*2,:w0*2]
|
| 577 |
+
return res
|
| 578 |
+
class UpCunet3x(nn.Module):
|
| 579 |
+
def __init__(self, in_channels=3, out_channels=3):
|
| 580 |
+
super(UpCunet3x, self).__init__()
|
| 581 |
+
self.unet1 = UNet1x3(in_channels, out_channels, deconv=True)
|
| 582 |
+
self.unet2 = UNet2(in_channels, out_channels, deconv=False)
|
| 583 |
+
def forward(self, x,tile_mode,cache_mode,alpha,pro):
|
| 584 |
+
n, c, h0, w0 = x.shape
|
| 585 |
+
if("Half" in x.type()):if_half=True
|
| 586 |
+
else:if_half=False
|
| 587 |
+
if(tile_mode==0):#不tile
|
| 588 |
+
ph = ((h0 - 1) // 4 + 1) * 4
|
| 589 |
+
pw = ((w0 - 1) // 4 + 1) * 4
|
| 590 |
+
x = F.pad(x, (14, 14 + pw - w0, 14, 14 + ph - h0), 'reflect') # 需要保证被2整除
|
| 591 |
+
x = self.unet1.forward(x)
|
| 592 |
+
x0 = self.unet2.forward(x,alpha)
|
| 593 |
+
x = F.pad(x, (-20, -20, -20, -20))
|
| 594 |
+
x = torch.add(x0, x)
|
| 595 |
+
if (w0 != pw or h0 != ph): x = x[:, :, :h0 * 3, :w0 * 3]
|
| 596 |
+
if(pro):
|
| 597 |
+
return ((x-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 598 |
+
else:
|
| 599 |
+
return (x * 255).round().clamp_(0, 255).byte()
|
| 600 |
+
elif(tile_mode==1):# 对长边减半
|
| 601 |
+
if(w0>=h0):
|
| 602 |
+
crop_size_w=((w0-1)//8*8+8)//2#减半后能被2整除,所以要先被4整除
|
| 603 |
+
crop_size_h=(h0-1)//4*4+4#能被2整除
|
| 604 |
+
else:
|
| 605 |
+
crop_size_h=((h0-1)//8*8+8)//2#减半后能被2整除,所以要先被4整除
|
| 606 |
+
crop_size_w=(w0-1)//4*4+4#能被2整除
|
| 607 |
+
crop_size=(crop_size_h,crop_size_w)
|
| 608 |
+
elif (tile_mode >= 2):
|
| 609 |
+
tile_mode=min(min(h0,w0)//128,int(tile_mode))#最小短边为128*128
|
| 610 |
+
t4 = tile_mode * 4
|
| 611 |
+
crop_size = (((h0 - 1) // t4 * t4 + t4) // tile_mode, ((w0 - 1) // t4 * t4 + t4) // tile_mode)
|
| 612 |
+
else:
|
| 613 |
+
print("tile_mode config error")
|
| 614 |
+
os._exit(233)
|
| 615 |
+
ph = ((h0 - 1) // crop_size[0] + 1) * crop_size[0]
|
| 616 |
+
pw = ((w0 - 1) // crop_size[1] + 1) * crop_size[1]
|
| 617 |
+
x=F.pad(x,(14,14+pw-w0,14,14+ph-h0),'reflect')
|
| 618 |
+
n,c,h,w=x.shape
|
| 619 |
+
if (if_half):se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 620 |
+
else:se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 621 |
+
n_patch=0
|
| 622 |
+
tmp_dict={}
|
| 623 |
+
for i in range(0,h-28,crop_size[0]):
|
| 624 |
+
tmp_dict[i]={}
|
| 625 |
+
for j in range(0,w-28,crop_size[1]):
|
| 626 |
+
x_crop=x[:,:,i:i+crop_size[0]+28,j:j+crop_size[1]+28]
|
| 627 |
+
n,c1,h1,w1=x_crop.shape
|
| 628 |
+
tmp0,x_crop = self.unet1.forward_a(x_crop)
|
| 629 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 630 |
+
tmp_se_mean = torch.mean(x_crop.float(), dim=(2, 3),keepdim=True).half()
|
| 631 |
+
else:
|
| 632 |
+
tmp_se_mean = torch.mean(x_crop, dim=(2, 3),keepdim=True)
|
| 633 |
+
se_mean0+=tmp_se_mean
|
| 634 |
+
n_patch+=1
|
| 635 |
+
tmp_dict[i][j]=(tmp0,x_crop)
|
| 636 |
+
se_mean0/=n_patch
|
| 637 |
+
if (if_half):se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 638 |
+
else:se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 639 |
+
for i in range(0,h-28,crop_size[0]):
|
| 640 |
+
for j in range(0,w-28,crop_size[1]):
|
| 641 |
+
tmp0, x_crop=tmp_dict[i][j]
|
| 642 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 643 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 644 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 645 |
+
opt_unet1 = F.pad(opt_unet1,(-20,-20,-20,-20))
|
| 646 |
+
if(cache_mode):opt_unet1,tmp_x1=q(opt_unet1,cache_mode), q(tmp_x1,cache_mode)
|
| 647 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 648 |
+
tmp_se_mean = torch.mean(tmp_x2.float(), dim=(2, 3),keepdim=True).half()
|
| 649 |
+
else:
|
| 650 |
+
tmp_se_mean = torch.mean(tmp_x2, dim=(2, 3),keepdim=True)
|
| 651 |
+
if(cache_mode):tmp_x2=q(tmp_x2,cache_mode)
|
| 652 |
+
se_mean1+=tmp_se_mean
|
| 653 |
+
tmp_dict[i][j]=(opt_unet1,tmp_x1,tmp_x2)
|
| 654 |
+
se_mean1/=n_patch
|
| 655 |
+
if (if_half):se_mean0=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 656 |
+
else:se_mean0=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 657 |
+
for i in range(0,h-28,crop_size[0]):
|
| 658 |
+
for j in range(0,w-28,crop_size[1]):
|
| 659 |
+
opt_unet1,tmp_x1, tmp_x2=tmp_dict[i][j]
|
| 660 |
+
if(cache_mode):tmp_x2=dq(tmp_x2[0],if_half,cache_mode,tmp_x2[1],tmp_x2[2],tmp_x2[3])
|
| 661 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1)
|
| 662 |
+
tmp_x2,tmp_x3=self.unet2.forward_b(tmp_x2)
|
| 663 |
+
if(cache_mode):tmp_x2=q(tmp_x2,cache_mode)
|
| 664 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 665 |
+
tmp_se_mean = torch.mean(tmp_x3.float(), dim=(2, 3),keepdim=True).half()
|
| 666 |
+
else:
|
| 667 |
+
tmp_se_mean = torch.mean(tmp_x3, dim=(2, 3),keepdim=True)
|
| 668 |
+
if(cache_mode):tmp_x3=q(tmp_x3,cache_mode)
|
| 669 |
+
se_mean0+=tmp_se_mean
|
| 670 |
+
tmp_dict[i][j]=(opt_unet1,tmp_x1,tmp_x2,tmp_x3)
|
| 671 |
+
se_mean0/=n_patch
|
| 672 |
+
if (if_half):se_mean1=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 673 |
+
else:se_mean1=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 674 |
+
for i in range(0,h-28,crop_size[0]):
|
| 675 |
+
for j in range(0,w-28,crop_size[1]):
|
| 676 |
+
opt_unet1,tmp_x1, tmp_x2,tmp_x3=tmp_dict[i][j]
|
| 677 |
+
if(cache_mode):tmp_x3=dq(tmp_x3[0],if_half,cache_mode,tmp_x3[1],tmp_x3[2],tmp_x3[3])
|
| 678 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean0)
|
| 679 |
+
if(cache_mode):tmp_x2=dq(tmp_x2[0],if_half,cache_mode,tmp_x2[1],tmp_x2[2],tmp_x2[3])
|
| 680 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)*alpha
|
| 681 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 682 |
+
tmp_se_mean = torch.mean(tmp_x4.float(), dim=(2, 3),keepdim=True).half()
|
| 683 |
+
else:
|
| 684 |
+
tmp_se_mean = torch.mean(tmp_x4, dim=(2, 3),keepdim=True)
|
| 685 |
+
if(cache_mode):tmp_x4=q(tmp_x4,cache_mode)
|
| 686 |
+
se_mean1+=tmp_se_mean
|
| 687 |
+
tmp_dict[i][j]=(opt_unet1,tmp_x1,tmp_x4)
|
| 688 |
+
se_mean1/=n_patch
|
| 689 |
+
res = torch.zeros((n, c, h * 3 - 84, w * 3 - 84),dtype=torch.uint8,device=x.device)
|
| 690 |
+
for i in range(0,h-28,crop_size[0]):
|
| 691 |
+
for j in range(0,w-28,crop_size[1]):
|
| 692 |
+
x,tmp_x1, tmp_x4=tmp_dict[i][j]
|
| 693 |
+
if(cache_mode):tmp_x4=dq(tmp_x4[0],if_half,cache_mode,tmp_x4[1],tmp_x4[2],tmp_x4[3])
|
| 694 |
+
tmp_x4=self.unet2.conv4.seblock.forward_mean(tmp_x4,se_mean1)
|
| 695 |
+
if(cache_mode):tmp_x1=dq(tmp_x1[0],if_half,cache_mode,tmp_x1[1],tmp_x1[2],tmp_x1[3])
|
| 696 |
+
x0=self.unet2.forward_d(tmp_x1,tmp_x4)
|
| 697 |
+
if(cache_mode):x = dq(x[0], if_half, cache_mode,x[1], x[2], x[3])
|
| 698 |
+
del tmp_dict[i][j]
|
| 699 |
+
x = torch.add(x0, x)#x0是unet2的最终输出
|
| 700 |
+
if(pro):
|
| 701 |
+
res[:, :, i * 3:i * 3 + h1 * 3 - 84, j * 3:j * 3 + w1 * 3 - 84] = ((x-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 702 |
+
else:
|
| 703 |
+
res[:, :, i * 3:i * 3 + h1 * 3 - 84, j * 3:j * 3 + w1 * 3 - 84] = (x*255).round().clamp_(0, 255).byte()
|
| 704 |
+
del tmp_dict
|
| 705 |
+
#torch.cuda.empty_cache()
|
| 706 |
+
if(w0!=pw or h0!=ph):res=res[:,:,:h0*3,:w0*3]
|
| 707 |
+
return res
|
| 708 |
+
def forward_gap_sync(self, x,tile_mode,alpha,pro):
|
| 709 |
+
n, c, h0, w0 = x.shape
|
| 710 |
+
if("Half" in x.type()):if_half=True
|
| 711 |
+
else:if_half=False
|
| 712 |
+
if(tile_mode==0):#不tile
|
| 713 |
+
ph = ((h0 - 1) // 4 + 1) * 4
|
| 714 |
+
pw = ((w0 - 1) // 4 + 1) * 4
|
| 715 |
+
x = F.pad(x, (14, 14 + pw - w0, 14, 14 + ph - h0), 'reflect') # 需要保证被2整除
|
| 716 |
+
x = self.unet1.forward(x)
|
| 717 |
+
x0 = self.unet2.forward(x,alpha)
|
| 718 |
+
x = F.pad(x, (-20, -20, -20, -20))
|
| 719 |
+
x = torch.add(x0, x)
|
| 720 |
+
if (w0 != pw or h0 != ph): x = x[:, :, :h0 * 3, :w0 * 3]
|
| 721 |
+
if(pro):
|
| 722 |
+
return ((x-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 723 |
+
else:
|
| 724 |
+
return (x * 255).round().clamp_(0, 255).byte()
|
| 725 |
+
elif(tile_mode==1):# 对长边减半
|
| 726 |
+
if(w0>=h0):
|
| 727 |
+
crop_size_w=((w0-1)//8*8+8)//2#减半后能被2整除,所以要先被4整除
|
| 728 |
+
crop_size_h=(h0-1)//4*4+4#能被2整除
|
| 729 |
+
else:
|
| 730 |
+
crop_size_h=((h0-1)//8*8+8)//2#减半后能被2整除,所以要先被4整除
|
| 731 |
+
crop_size_w=(w0-1)//4*4+4#能被2整除
|
| 732 |
+
crop_size=(crop_size_h,crop_size_w)
|
| 733 |
+
elif (tile_mode >= 2):
|
| 734 |
+
tile_mode=min(min(h0,w0)//128,int(tile_mode))#最小短边为128*128
|
| 735 |
+
t4 = tile_mode * 4
|
| 736 |
+
crop_size = (((h0 - 1) // t4 * t4 + t4) // tile_mode, ((w0 - 1) // t4 * t4 + t4) // tile_mode)
|
| 737 |
+
else:
|
| 738 |
+
print("tile_mode config error")
|
| 739 |
+
os._exit(233)
|
| 740 |
+
ph = ((h0 - 1) // crop_size[0] + 1) * crop_size[0]
|
| 741 |
+
pw = ((w0 - 1) // crop_size[1] + 1) * crop_size[1]
|
| 742 |
+
x=F.pad(x,(14,14+pw-w0,14,14+ph-h0),'reflect')
|
| 743 |
+
n,c,h,w=x.shape
|
| 744 |
+
if (if_half):se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 745 |
+
else:se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 746 |
+
n_patch=0
|
| 747 |
+
h1,w1=crop_size[0]+28,crop_size[1]+28
|
| 748 |
+
######stage1
|
| 749 |
+
for i in range(0,h-28,crop_size[0]):
|
| 750 |
+
for j in range(0,w-28,crop_size[1]):
|
| 751 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+28,j:j+crop_size[1]+28])
|
| 752 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 753 |
+
tmp_se_mean = torch.mean(x_crop.float(), dim=(2, 3),keepdim=True).half()
|
| 754 |
+
else:
|
| 755 |
+
tmp_se_mean = torch.mean(x_crop, dim=(2, 3),keepdim=True)
|
| 756 |
+
se_mean0+=tmp_se_mean
|
| 757 |
+
n_patch+=1
|
| 758 |
+
se_mean0/=n_patch
|
| 759 |
+
######stage1+state2
|
| 760 |
+
if (if_half):se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 761 |
+
else:se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 762 |
+
for i in range(0,h-28,crop_size[0]):
|
| 763 |
+
for j in range(0,w-28,crop_size[1]):
|
| 764 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+28,j:j+crop_size[1]+28])
|
| 765 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 766 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 767 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 768 |
+
if (if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 769 |
+
tmp_se_mean = torch.mean(tmp_x2.float(), dim=(2, 3),keepdim=True).half()
|
| 770 |
+
else:
|
| 771 |
+
tmp_se_mean = torch.mean(tmp_x2, dim=(2, 3),keepdim=True)
|
| 772 |
+
se_mean1+=tmp_se_mean
|
| 773 |
+
se_mean1/=n_patch
|
| 774 |
+
######stage1+state2+state3
|
| 775 |
+
if (if_half):se_mean2=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 776 |
+
else:se_mean2=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 777 |
+
for i in range(0,h-28,crop_size[0]):
|
| 778 |
+
for j in range(0,w-28,crop_size[1]):
|
| 779 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+28,j:j+crop_size[1]+28])
|
| 780 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 781 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 782 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 783 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1)
|
| 784 |
+
tmp_x2,tmp_x3=self.unet2.forward_b(tmp_x2)
|
| 785 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 786 |
+
tmp_se_mean = torch.mean(tmp_x3.float(), dim=(2, 3),keepdim=True).half()
|
| 787 |
+
else:
|
| 788 |
+
tmp_se_mean = torch.mean(tmp_x3, dim=(2, 3),keepdim=True)
|
| 789 |
+
se_mean2+=tmp_se_mean
|
| 790 |
+
se_mean2/=n_patch
|
| 791 |
+
#########stage1+state2+state3+stage4
|
| 792 |
+
if (if_half):se_mean3=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 793 |
+
else:se_mean3=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 794 |
+
for i in range(0,h-28,crop_size[0]):
|
| 795 |
+
for j in range(0,w-28,crop_size[1]):
|
| 796 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+28,j:j+crop_size[1]+28])
|
| 797 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 798 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 799 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 800 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1)
|
| 801 |
+
tmp_x2,tmp_x3=self.unet2.forward_b(tmp_x2)
|
| 802 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean2)
|
| 803 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)
|
| 804 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 805 |
+
tmp_se_mean = torch.mean(tmp_x4.float(), dim=(2, 3),keepdim=True).half()
|
| 806 |
+
else:
|
| 807 |
+
tmp_se_mean = torch.mean(tmp_x4, dim=(2, 3),keepdim=True)
|
| 808 |
+
se_mean3+=tmp_se_mean
|
| 809 |
+
se_mean3/=n_patch
|
| 810 |
+
###########stage1+state2+state3+stage4+stage_tail
|
| 811 |
+
res = torch.zeros((n, c, h * 3 - 84, w * 3 - 84),dtype=torch.uint8,device=x.device)
|
| 812 |
+
for i in range(0,h-28,crop_size[0]):
|
| 813 |
+
for j in range(0,w-28,crop_size[1]):
|
| 814 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+28,j:j+crop_size[1]+28])
|
| 815 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 816 |
+
x_crop=self.unet1.forward_b(tmp0,x_crop)
|
| 817 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(x_crop)
|
| 818 |
+
x_crop = F.pad(x_crop,(-20,-20,-20,-20))
|
| 819 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1)
|
| 820 |
+
tmp_x2, tmp_x3 = self.unet2.forward_b(tmp_x2)
|
| 821 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean2)
|
| 822 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)*alpha
|
| 823 |
+
tmp_x4=self.unet2.conv4.seblock.forward_mean(tmp_x4,se_mean3)
|
| 824 |
+
x0=self.unet2.forward_d(tmp_x1,tmp_x4)
|
| 825 |
+
x_crop = torch.add(x0, x_crop)
|
| 826 |
+
if(pro):
|
| 827 |
+
res[:, :, i * 3:i * 3 + h1 * 3 - 84, j * 3:j * 3 + w1 * 3 - 84] = ((x_crop-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 828 |
+
else:
|
| 829 |
+
res[:, :, i * 3:i * 3 + h1 * 3 - 84, j * 3:j * 3 + w1 * 3 - 84] = (x_crop* 255.0).round().clamp_(0, 255).byte()
|
| 830 |
+
#torch.cuda.empty_cache()
|
| 831 |
+
if(w0!=pw or h0!=ph):res=res[:,:,:h0*3,:w0*3]
|
| 832 |
+
return res
|
| 833 |
+
def forward_fast_rough(self, x,tile_mode,alpha,pro):#1.7G
|
| 834 |
+
n, c, h0, w0 = x.shape
|
| 835 |
+
if("Half" in x.type()):if_half=True
|
| 836 |
+
else:if_half=False
|
| 837 |
+
if(tile_mode<3):return self.forward(x,tile_mode,1,alpha,pro)#至少切成3x3
|
| 838 |
+
elif(tile_mode>=3):
|
| 839 |
+
tile_mode=min(min(h0,w0)//128,int(tile_mode))#最小短边为128*128
|
| 840 |
+
if (tile_mode < 3): return self.forward(x, tile_mode, 1, alpha, pro)
|
| 841 |
+
t4 = tile_mode * 4
|
| 842 |
+
crop_size = (((h0 - 1) // t4 * t4 + t4) // tile_mode, ((w0 - 1) // t4 * t4 + t4) // tile_mode) # 5.6G
|
| 843 |
+
ph = ((h0 - 1) // crop_size[0] + 1) * crop_size[0]
|
| 844 |
+
pw = ((w0 - 1) // crop_size[1] + 1) * crop_size[1]
|
| 845 |
+
x=F.pad(x,(14,14+pw-w0,14,14+ph-h0),'reflect')
|
| 846 |
+
n,c,h,w=x.shape
|
| 847 |
+
h1,w1=crop_size[0]+28,crop_size[1]+28
|
| 848 |
+
n_patch=0
|
| 849 |
+
###########stage1+state2+state3+stage4
|
| 850 |
+
if (if_half):se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 851 |
+
else:se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 852 |
+
if (if_half):se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 853 |
+
else:se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 854 |
+
if (if_half):se_mean2=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 855 |
+
else:se_mean2=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 856 |
+
if (if_half):se_mean3=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 857 |
+
else:se_mean3=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 858 |
+
for i in range(0,h-28,crop_size[0]):
|
| 859 |
+
if((i//crop_size[0])%2==0):continue
|
| 860 |
+
for j in range(0,w-28,crop_size[1]):
|
| 861 |
+
if ((j//crop_size[1]) % 2 == 0): continue
|
| 862 |
+
n_patch+=1
|
| 863 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+28,j:j+crop_size[1]+28])
|
| 864 |
+
if(if_half):se_mean0 += torch.mean(x_crop.float(), dim=(2, 3),keepdim=True).half()
|
| 865 |
+
else:se_mean0 += torch.mean(x_crop, dim=(2, 3),keepdim=True)
|
| 866 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0/n_patch)
|
| 867 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 868 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 869 |
+
if(if_half):se_mean1 += torch.mean(tmp_x2.float(), dim=(2, 3),keepdim=True).half()
|
| 870 |
+
else:se_mean1 += torch.mean(tmp_x2, dim=(2, 3),keepdim=True)
|
| 871 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1/n_patch)
|
| 872 |
+
tmp_x2,tmp_x3=self.unet2.forward_b(tmp_x2)
|
| 873 |
+
if(if_half):se_mean2 += torch.mean(tmp_x3.float(), dim=(2, 3),keepdim=True).half()
|
| 874 |
+
else:se_mean2 += torch.mean(tmp_x3, dim=(2, 3),keepdim=True)
|
| 875 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean2/n_patch)
|
| 876 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)
|
| 877 |
+
if(if_half):se_mean3 += torch.mean(tmp_x4.float(), dim=(2, 3),keepdim=True).half()
|
| 878 |
+
else:se_mean3 += torch.mean(tmp_x4, dim=(2, 3),keepdim=True)
|
| 879 |
+
# print("3x-n_patch=%s,tile_mode=%s" % (n_patch,tile_mode))
|
| 880 |
+
###########stage1+state2+state3+stage4+stage_tail
|
| 881 |
+
res = torch.zeros((n, c, h * 3 - 84, w * 3 - 84),dtype=torch.uint8,device=x.device)
|
| 882 |
+
for i in range(0,h-28,crop_size[0]):
|
| 883 |
+
for j in range(0,w-28,crop_size[1]):
|
| 884 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+28,j:j+crop_size[1]+28])
|
| 885 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0/n_patch)
|
| 886 |
+
x_crop=self.unet1.forward_b(tmp0,x_crop)
|
| 887 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(x_crop)
|
| 888 |
+
x_crop = F.pad(x_crop,(-20,-20,-20,-20))
|
| 889 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1/n_patch)
|
| 890 |
+
tmp_x2, tmp_x3 = self.unet2.forward_b(tmp_x2)
|
| 891 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean2/n_patch)
|
| 892 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)*alpha
|
| 893 |
+
tmp_x4=self.unet2.conv4.seblock.forward_mean(tmp_x4,se_mean3/n_patch)
|
| 894 |
+
x0=self.unet2.forward_d(tmp_x1,tmp_x4)
|
| 895 |
+
x_crop = torch.add(x0, x_crop)
|
| 896 |
+
if(pro):
|
| 897 |
+
res[:, :, i * 3:i * 3 + h1 * 3 - 84, j * 3:j * 3 + w1 * 3 - 84] = ((x_crop-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 898 |
+
else:
|
| 899 |
+
res[:, :, i * 3:i * 3 + h1 * 3 - 84, j * 3:j * 3 + w1 * 3 - 84] = (x_crop* 255.0).round().clamp_(0, 255).byte()
|
| 900 |
+
#torch.cuda.empty_cache()
|
| 901 |
+
if(w0!=pw or h0!=ph):res=res[:,:,:h0*3,:w0*3]
|
| 902 |
+
return res
|
| 903 |
+
class UpCunet4x(nn.Module):
|
| 904 |
+
def __init__(self, in_channels=3, out_channels=3):
|
| 905 |
+
super(UpCunet4x, self).__init__()
|
| 906 |
+
self.unet1 = UNet1(in_channels, 64, deconv=True)
|
| 907 |
+
self.unet2 = UNet2(64, 64, deconv=False)
|
| 908 |
+
self.ps=nn.PixelShuffle(2)
|
| 909 |
+
self.conv_final=nn.Conv2d(64,12,3,1,padding=0,bias=True)
|
| 910 |
+
def forward(self, x,tile_mode,cache_mode,alpha,pro):
|
| 911 |
+
n, c, h0, w0 = x.shape
|
| 912 |
+
if("Half" in x.type()):if_half=True
|
| 913 |
+
else:if_half=False
|
| 914 |
+
x00 = x
|
| 915 |
+
if(tile_mode==0):#不tile
|
| 916 |
+
ph = ((h0 - 1) // 2 + 1) * 2
|
| 917 |
+
pw = ((w0 - 1) // 2 + 1) * 2
|
| 918 |
+
x = F.pad(x, (19, 19 + pw - w0, 19, 19 + ph - h0), 'reflect') # 需要保证被2整除
|
| 919 |
+
x = self.unet1.forward(x)
|
| 920 |
+
x0 = self.unet2.forward(x,alpha)
|
| 921 |
+
x1 = F.pad(x, (-20, -20, -20, -20))
|
| 922 |
+
x = torch.add(x0, x1)
|
| 923 |
+
x=self.conv_final(x)
|
| 924 |
+
x=F.pad(x,(-1,-1,-1,-1))
|
| 925 |
+
x=self.ps(x)
|
| 926 |
+
if (w0 != pw or h0 != ph): x = x[:, :, :h0 * 4, :w0 * 4]
|
| 927 |
+
x+=F.interpolate(x00, scale_factor=4, mode='nearest')
|
| 928 |
+
if(pro):
|
| 929 |
+
return ((x-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 930 |
+
else:
|
| 931 |
+
return (x * 255).round().clamp_(0, 255).byte()
|
| 932 |
+
elif(tile_mode==1):# 对长边减半
|
| 933 |
+
if(w0>=h0):
|
| 934 |
+
crop_size_w=((w0-1)//4*4+4)//2#减半后能被2整除,所以要先被4整除
|
| 935 |
+
crop_size_h=(h0-1)//2*2+2#能被2整除
|
| 936 |
+
else:
|
| 937 |
+
crop_size_h=((h0-1)//4*4+4)//2#减半后能被2整除,所以要先被4整除
|
| 938 |
+
crop_size_w=(w0-1)//2*2+2#能被2整除
|
| 939 |
+
crop_size=(crop_size_h,crop_size_w)
|
| 940 |
+
elif (tile_mode >= 2):
|
| 941 |
+
tile_mode=min(min(h0,w0)//128,int(tile_mode))#最小短边为128*128
|
| 942 |
+
t2 = tile_mode * 2
|
| 943 |
+
crop_size = (((h0 - 1) // t2 * t2 + t2) // tile_mode, ((w0 - 1) // t2 * t2 + t2) // tile_mode)
|
| 944 |
+
else:
|
| 945 |
+
print("tile_mode config error")
|
| 946 |
+
os._exit(233)
|
| 947 |
+
ph = ((h0 - 1) // crop_size[0] + 1) * crop_size[0]
|
| 948 |
+
pw = ((w0 - 1) // crop_size[1] + 1) * crop_size[1]
|
| 949 |
+
x=F.pad(x,(19,19+pw-w0,19,19+ph-h0),'reflect')
|
| 950 |
+
n,c,h,w=x.shape
|
| 951 |
+
if (if_half):se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 952 |
+
else:se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 953 |
+
n_patch=0
|
| 954 |
+
tmp_dict={}
|
| 955 |
+
for i in range(0,h-38,crop_size[0]):
|
| 956 |
+
tmp_dict[i]={}
|
| 957 |
+
for j in range(0,w-38,crop_size[1]):
|
| 958 |
+
x_crop=x[:,:,i:i+crop_size[0]+38,j:j+crop_size[1]+38]
|
| 959 |
+
n,c1,h1,w1=x_crop.shape
|
| 960 |
+
tmp0,x_crop = self.unet1.forward_a(x_crop)
|
| 961 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 962 |
+
tmp_se_mean = torch.mean(x_crop.float(), dim=(2, 3),keepdim=True).half()
|
| 963 |
+
else:
|
| 964 |
+
tmp_se_mean = torch.mean(x_crop, dim=(2, 3),keepdim=True)
|
| 965 |
+
se_mean0+=tmp_se_mean
|
| 966 |
+
n_patch+=1
|
| 967 |
+
tmp_dict[i][j]=(tmp0,x_crop)
|
| 968 |
+
se_mean0/=n_patch
|
| 969 |
+
if (if_half):se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 970 |
+
else:se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 971 |
+
for i in range(0,h-38,crop_size[0]):
|
| 972 |
+
for j in range(0,w-38,crop_size[1]):
|
| 973 |
+
tmp0, x_crop=tmp_dict[i][j]
|
| 974 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 975 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 976 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 977 |
+
opt_unet1 = F.pad(opt_unet1,(-20,-20,-20,-20))
|
| 978 |
+
if(cache_mode):opt_unet1,tmp_x1=q(opt_unet1,cache_mode), q(tmp_x1,cache_mode)
|
| 979 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 980 |
+
tmp_se_mean = torch.mean(tmp_x2.float(), dim=(2, 3),keepdim=True).half()
|
| 981 |
+
else:
|
| 982 |
+
tmp_se_mean = torch.mean(tmp_x2, dim=(2, 3),keepdim=True)
|
| 983 |
+
if(cache_mode):tmp_x2=q(tmp_x2,cache_mode)
|
| 984 |
+
se_mean1+=tmp_se_mean
|
| 985 |
+
tmp_dict[i][j]=(opt_unet1,tmp_x1,tmp_x2)
|
| 986 |
+
se_mean1/=n_patch
|
| 987 |
+
if (if_half):se_mean0=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 988 |
+
else:se_mean0=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 989 |
+
for i in range(0,h-38,crop_size[0]):
|
| 990 |
+
for j in range(0,w-38,crop_size[1]):
|
| 991 |
+
opt_unet1,tmp_x1, tmp_x2=tmp_dict[i][j]
|
| 992 |
+
if(cache_mode):tmp_x2=dq(tmp_x2[0],if_half,cache_mode,tmp_x2[1],tmp_x2[2],tmp_x2[3])
|
| 993 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1)
|
| 994 |
+
tmp_x2, tmp_x3 = self.unet2.forward_b(tmp_x2)
|
| 995 |
+
if(cache_mode):tmp_x2=q(tmp_x2,cache_mode)
|
| 996 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 997 |
+
tmp_se_mean = torch.mean(tmp_x3.float(), dim=(2, 3),keepdim=True).half()
|
| 998 |
+
else:
|
| 999 |
+
tmp_se_mean = torch.mean(tmp_x3, dim=(2, 3),keepdim=True)
|
| 1000 |
+
if(cache_mode):tmp_x3=q(tmp_x3,cache_mode)
|
| 1001 |
+
se_mean0+=tmp_se_mean
|
| 1002 |
+
tmp_dict[i][j]=(opt_unet1,tmp_x1,tmp_x2,tmp_x3)
|
| 1003 |
+
se_mean0/=n_patch
|
| 1004 |
+
if (if_half):se_mean1=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 1005 |
+
else:se_mean1=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 1006 |
+
for i in range(0,h-38,crop_size[0]):
|
| 1007 |
+
for j in range(0,w-38,crop_size[1]):
|
| 1008 |
+
opt_unet1,tmp_x1, tmp_x2,tmp_x3=tmp_dict[i][j]
|
| 1009 |
+
if(cache_mode):tmp_x3=dq(tmp_x3[0],if_half,cache_mode,tmp_x3[1],tmp_x3[2],tmp_x3[3])
|
| 1010 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean0)
|
| 1011 |
+
if(cache_mode):tmp_x2=dq(tmp_x2[0],if_half,cache_mode,tmp_x2[1],tmp_x2[2],tmp_x2[3])
|
| 1012 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)*alpha
|
| 1013 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 1014 |
+
tmp_se_mean = torch.mean(tmp_x4.float(), dim=(2, 3),keepdim=True).half()
|
| 1015 |
+
else:
|
| 1016 |
+
tmp_se_mean = torch.mean(tmp_x4, dim=(2, 3),keepdim=True)
|
| 1017 |
+
if(cache_mode):tmp_x4=q(tmp_x4,cache_mode)
|
| 1018 |
+
se_mean1+=tmp_se_mean
|
| 1019 |
+
tmp_dict[i][j]=(opt_unet1,tmp_x1,tmp_x4)
|
| 1020 |
+
se_mean1/=n_patch
|
| 1021 |
+
res = torch.zeros((n, c, h * 4 - 152, w * 4 - 152),dtype=torch.uint8,device=x.device)
|
| 1022 |
+
for i in range(0,h-38,crop_size[0]):
|
| 1023 |
+
for j in range(0,w-38,crop_size[1]):
|
| 1024 |
+
x,tmp_x1, tmp_x4=tmp_dict[i][j]
|
| 1025 |
+
if(cache_mode):tmp_x4=dq(tmp_x4[0],if_half,cache_mode,tmp_x4[1],tmp_x4[2],tmp_x4[3])
|
| 1026 |
+
tmp_x4=self.unet2.conv4.seblock.forward_mean(tmp_x4,se_mean1)
|
| 1027 |
+
if(cache_mode):tmp_x1=dq(tmp_x1[0],if_half,cache_mode,tmp_x1[1],tmp_x1[2],tmp_x1[3])
|
| 1028 |
+
x0=self.unet2.forward_d(tmp_x1,tmp_x4)
|
| 1029 |
+
del tmp_x1,tmp_x4
|
| 1030 |
+
if(cache_mode):x = dq(x[0], if_half, cache_mode,x[1], x[2], x[3])
|
| 1031 |
+
del tmp_dict[i][j]
|
| 1032 |
+
x = torch.add(x0, x)#x0是unet2的最终输出
|
| 1033 |
+
x=self.conv_final(x)
|
| 1034 |
+
x = F.pad(x, (-1, -1, -1, -1))
|
| 1035 |
+
x=self.ps(x)
|
| 1036 |
+
x00_crop=x00[:, :, i:i + h1 - 38, j:j + w1 - 38]
|
| 1037 |
+
_,_,h2,w2=x00_crop.shape
|
| 1038 |
+
x[:,:,:h2*4,:w2*4]+=F.interpolate(x00_crop, scale_factor=4, mode='nearest')
|
| 1039 |
+
if(pro):
|
| 1040 |
+
res[:, :, i * 4:i * 4 + h1 * 4 - 152, j * 4:j * 4 + w1 * 4 - 152] = ((x-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 1041 |
+
else:
|
| 1042 |
+
res[:, :, i * 4:i * 4 + h1 * 4 - 152, j * 4:j * 4 + w1 * 4 - 152] = (x*255).round().clamp_(0, 255).byte()
|
| 1043 |
+
del tmp_dict
|
| 1044 |
+
#torch.cuda.empty_cache()
|
| 1045 |
+
if(w0!=pw or h0!=ph):res=res[:,:,:h0*4,:w0*4]
|
| 1046 |
+
return res
|
| 1047 |
+
def forward_gap_sync(self, x,tile_mode,alpha,pro):
|
| 1048 |
+
n, c, h0, w0 = x.shape
|
| 1049 |
+
if("Half" in x.type()):if_half=True
|
| 1050 |
+
else:if_half=False
|
| 1051 |
+
x00 = x
|
| 1052 |
+
if(tile_mode==0):#不tile
|
| 1053 |
+
ph = ((h0 - 1) // 2 + 1) * 2
|
| 1054 |
+
pw = ((w0 - 1) // 2 + 1) * 2
|
| 1055 |
+
x = F.pad(x, (19, 19 + pw - w0, 19, 19 + ph - h0), 'reflect') # 需要保证被2整除
|
| 1056 |
+
x = self.unet1.forward(x)
|
| 1057 |
+
x0 = self.unet2.forward(x,alpha)
|
| 1058 |
+
x1 = F.pad(x, (-20, -20, -20, -20))
|
| 1059 |
+
x = torch.add(x0, x1)
|
| 1060 |
+
x=self.conv_final(x)
|
| 1061 |
+
x=F.pad(x,(-1,-1,-1,-1))
|
| 1062 |
+
x=self.ps(x)
|
| 1063 |
+
if (w0 != pw or h0 != ph): x = x[:, :, :h0 * 4, :w0 * 4]
|
| 1064 |
+
x+=F.interpolate(x00, scale_factor=4, mode='nearest')
|
| 1065 |
+
if(pro):
|
| 1066 |
+
return ((x-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 1067 |
+
else:
|
| 1068 |
+
return (x * 255).round().clamp_(0, 255).byte()
|
| 1069 |
+
elif(tile_mode==1):# 对长边减半
|
| 1070 |
+
if(w0>=h0):
|
| 1071 |
+
crop_size_w=((w0-1)//4*4+4)//2#减半后能被2整除,所以要先被4整除
|
| 1072 |
+
crop_size_h=(h0-1)//2*2+2#能被2整除
|
| 1073 |
+
else:
|
| 1074 |
+
crop_size_h=((h0-1)//4*4+4)//2#减半后能被2整除,所以要先被4整除
|
| 1075 |
+
crop_size_w=(w0-1)//2*2+2#能被2整除
|
| 1076 |
+
crop_size=(crop_size_h,crop_size_w)
|
| 1077 |
+
elif(tile_mode>=2):#hw都减半
|
| 1078 |
+
tile_mode=min(min(h0,w0)//128,int(tile_mode))#最小短边为128*128
|
| 1079 |
+
t2=tile_mode*2
|
| 1080 |
+
crop_size=(((h0-1)//t2*t2+t2)//tile_mode,((w0-1)//t2*t2+t2)//tile_mode)#5.6G
|
| 1081 |
+
else:
|
| 1082 |
+
print("tile_mode config error")
|
| 1083 |
+
os._exit(233)
|
| 1084 |
+
ph = ((h0 - 1) // crop_size[0] + 1) * crop_size[0]
|
| 1085 |
+
pw = ((w0 - 1) // crop_size[1] + 1) * crop_size[1]
|
| 1086 |
+
x=F.pad(x,(19,19+pw-w0,19,19+ph-h0),'reflect')
|
| 1087 |
+
n,c,h,w=x.shape
|
| 1088 |
+
if (if_half):se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 1089 |
+
else:se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 1090 |
+
n_patch=0
|
| 1091 |
+
h1,w1=crop_size[0]+38,crop_size[1]+38
|
| 1092 |
+
######stage1
|
| 1093 |
+
for i in range(0,h-38,crop_size[0]):
|
| 1094 |
+
for j in range(0,w-38,crop_size[1]):
|
| 1095 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+38,j:j+crop_size[1]+38])
|
| 1096 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 1097 |
+
tmp_se_mean = torch.mean(x_crop.float(), dim=(2, 3),keepdim=True).half()
|
| 1098 |
+
else:
|
| 1099 |
+
tmp_se_mean = torch.mean(x_crop, dim=(2, 3),keepdim=True)
|
| 1100 |
+
se_mean0+=tmp_se_mean
|
| 1101 |
+
n_patch+=1
|
| 1102 |
+
se_mean0/=n_patch
|
| 1103 |
+
######stage1+state2
|
| 1104 |
+
if (if_half):se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 1105 |
+
else:se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 1106 |
+
for i in range(0,h-38,crop_size[0]):
|
| 1107 |
+
for j in range(0,w-38,crop_size[1]):
|
| 1108 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+38,j:j+crop_size[1]+38])
|
| 1109 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 1110 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 1111 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 1112 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 1113 |
+
tmp_se_mean = torch.mean(tmp_x2.float(), dim=(2, 3),keepdim=True).half()
|
| 1114 |
+
else:
|
| 1115 |
+
tmp_se_mean = torch.mean(tmp_x2, dim=(2, 3),keepdim=True)
|
| 1116 |
+
se_mean1+=tmp_se_mean
|
| 1117 |
+
se_mean1/=n_patch
|
| 1118 |
+
######stage1+state2+state3
|
| 1119 |
+
if (if_half):se_mean2=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 1120 |
+
else:se_mean2=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 1121 |
+
for i in range(0,h-38,crop_size[0]):
|
| 1122 |
+
for j in range(0,w-38,crop_size[1]):
|
| 1123 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+38,j:j+crop_size[1]+38])
|
| 1124 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 1125 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 1126 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 1127 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1)
|
| 1128 |
+
tmp_x2,tmp_x3=self.unet2.forward_b(tmp_x2)
|
| 1129 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 1130 |
+
tmp_se_mean = torch.mean(tmp_x3.float(), dim=(2, 3),keepdim=True).half()
|
| 1131 |
+
else:
|
| 1132 |
+
tmp_se_mean = torch.mean(tmp_x3, dim=(2, 3),keepdim=True)
|
| 1133 |
+
se_mean2+=tmp_se_mean
|
| 1134 |
+
se_mean2/=n_patch
|
| 1135 |
+
#########stage1+state2+state3+stage4
|
| 1136 |
+
if (if_half):se_mean3=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 1137 |
+
else:se_mean3=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 1138 |
+
for i in range(0,h-38,crop_size[0]):
|
| 1139 |
+
for j in range(0,w-38,crop_size[1]):
|
| 1140 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+38,j:j+crop_size[1]+38])
|
| 1141 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 1142 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 1143 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 1144 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1)
|
| 1145 |
+
tmp_x2,tmp_x3=self.unet2.forward_b(tmp_x2)
|
| 1146 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean2)
|
| 1147 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)
|
| 1148 |
+
if(if_half): # torch.HalfTensor/torch.cuda.HalfTensor
|
| 1149 |
+
tmp_se_mean = torch.mean(tmp_x4.float(), dim=(2, 3),keepdim=True).half()
|
| 1150 |
+
else:
|
| 1151 |
+
tmp_se_mean = torch.mean(tmp_x4, dim=(2, 3),keepdim=True)
|
| 1152 |
+
se_mean3+=tmp_se_mean
|
| 1153 |
+
se_mean3/=n_patch
|
| 1154 |
+
###########stage1+state2+state3+stage4+stage_tail
|
| 1155 |
+
res = torch.zeros((n, c, h * 4 - 152, w * 4 - 152),dtype=torch.uint8,device=x.device)
|
| 1156 |
+
for i in range(0,h-38,crop_size[0]):
|
| 1157 |
+
for j in range(0,w-38,crop_size[1]):
|
| 1158 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+38,j:j+crop_size[1]+38])
|
| 1159 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0)
|
| 1160 |
+
x_crop=self.unet1.forward_b(tmp0,x_crop)
|
| 1161 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(x_crop)
|
| 1162 |
+
x_crop = F.pad(x_crop,(-20,-20,-20,-20))
|
| 1163 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1)
|
| 1164 |
+
tmp_x2, tmp_x3 = self.unet2.forward_b(tmp_x2)
|
| 1165 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean2)
|
| 1166 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)*alpha
|
| 1167 |
+
tmp_x4=self.unet2.conv4.seblock.forward_mean(tmp_x4,se_mean3)
|
| 1168 |
+
x0=self.unet2.forward_d(tmp_x1,tmp_x4)
|
| 1169 |
+
x_crop = torch.add(x0, x_crop)
|
| 1170 |
+
x_crop=self.conv_final(x_crop)
|
| 1171 |
+
x_crop = F.pad(x_crop, (-1, -1, -1, -1))
|
| 1172 |
+
x_crop=self.ps(x_crop)
|
| 1173 |
+
x00_crop=x00[:, :, i:i + h1 - 38, j:j + w1 - 38]
|
| 1174 |
+
_,_,h2,w2=x00_crop.shape
|
| 1175 |
+
x_crop[:,:,:h2*4,:w2*4]+=F.interpolate(x00_crop, scale_factor=4, mode='nearest')
|
| 1176 |
+
if(pro):
|
| 1177 |
+
res[:, :, i * 4:i * 4 + h1 * 4 - 152, j * 4:j * 4 + w1 * 4 - 152] = ((x_crop-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 1178 |
+
else:
|
| 1179 |
+
res[:, :, i * 4:i * 4 + h1 * 4 - 152, j * 4:j * 4 + w1 * 4 - 152] = (x_crop*255).round().clamp_(0, 255).byte()
|
| 1180 |
+
#torch.cuda.empty_cache()
|
| 1181 |
+
if(w0!=pw or h0!=ph):res=res[:,:,:h0*4,:w0*4]
|
| 1182 |
+
return res
|
| 1183 |
+
def forward_fast_rough(self, x,tile_mode,alpha,pro):#1.7G
|
| 1184 |
+
n, c, h0, w0 = x.shape
|
| 1185 |
+
if("Half" in x.type()):if_half=True
|
| 1186 |
+
else:if_half=False
|
| 1187 |
+
x00 = x
|
| 1188 |
+
if(tile_mode<3):return self.forward(x,tile_mode,1,alpha,pro)#至少切成3x3
|
| 1189 |
+
elif(tile_mode>=3):
|
| 1190 |
+
tile_mode=min(min(h0,w0)//128,int(tile_mode))#最小短边为128*128
|
| 1191 |
+
if (tile_mode < 3): return self.forward(x, tile_mode, 1, alpha, pro)
|
| 1192 |
+
t2=tile_mode*2
|
| 1193 |
+
crop_size=(((h0-1)//t2*t2+t2)//tile_mode,((w0-1)//t2*t2+t2)//tile_mode)#5.6G
|
| 1194 |
+
ph = ((h0 - 1) // crop_size[0] + 1) * crop_size[0]
|
| 1195 |
+
pw = ((w0 - 1) // crop_size[1] + 1) * crop_size[1]
|
| 1196 |
+
x=F.pad(x,(19,19+pw-w0,19,19+ph-h0),'reflect')
|
| 1197 |
+
n,c,h,w=x.shape
|
| 1198 |
+
h1,w1=crop_size[0]+38,crop_size[1]+38
|
| 1199 |
+
n_patch=0
|
| 1200 |
+
###########stage1+state2+state3+stage4
|
| 1201 |
+
if (if_half):se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 1202 |
+
else:se_mean0=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 1203 |
+
if (if_half):se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 1204 |
+
else:se_mean1=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 1205 |
+
if (if_half):se_mean2=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float16)
|
| 1206 |
+
else:se_mean2=torch.zeros((n,128,1,1),device=x.device,dtype=torch.float32)
|
| 1207 |
+
if (if_half):se_mean3=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float16)
|
| 1208 |
+
else:se_mean3=torch.zeros((n,64,1,1),device=x.device,dtype=torch.float32)
|
| 1209 |
+
for i in range(0,h-38,crop_size[0]):
|
| 1210 |
+
if((i//crop_size[0])%2==0):continue
|
| 1211 |
+
for j in range(0,w-38,crop_size[1]):
|
| 1212 |
+
if ((j//crop_size[1]) % 2 == 0): continue
|
| 1213 |
+
n_patch+=1
|
| 1214 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+38,j:j+crop_size[1]+38])
|
| 1215 |
+
if(if_half):se_mean0 += torch.mean(x_crop.float(), dim=(2, 3),keepdim=True).half()
|
| 1216 |
+
else:se_mean0 += torch.mean(x_crop, dim=(2, 3),keepdim=True)
|
| 1217 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0/n_patch)
|
| 1218 |
+
opt_unet1=self.unet1.forward_b(tmp0,x_crop)
|
| 1219 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(opt_unet1)
|
| 1220 |
+
if(if_half):se_mean1 += torch.mean(tmp_x2.float(), dim=(2, 3),keepdim=True).half()
|
| 1221 |
+
else:se_mean1 += torch.mean(tmp_x2, dim=(2, 3),keepdim=True)
|
| 1222 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1/n_patch)
|
| 1223 |
+
tmp_x2,tmp_x3=self.unet2.forward_b(tmp_x2)
|
| 1224 |
+
if(if_half):se_mean2 += torch.mean(tmp_x3.float(), dim=(2, 3),keepdim=True).half()
|
| 1225 |
+
else:se_mean2 += torch.mean(tmp_x3, dim=(2, 3),keepdim=True)
|
| 1226 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean2/n_patch)
|
| 1227 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)
|
| 1228 |
+
if(if_half):se_mean3 += torch.mean(tmp_x4.float(), dim=(2, 3),keepdim=True).half()
|
| 1229 |
+
else:se_mean3 += torch.mean(tmp_x4, dim=(2, 3),keepdim=True)
|
| 1230 |
+
# print("4x-n_patch=%s,tile_mode=%s" % (n_patch,tile_mode))
|
| 1231 |
+
###########stage1+state2+state3+stage4+stage_tail
|
| 1232 |
+
res = torch.zeros((n, c, h * 4 - 152, w * 4 - 152),dtype=torch.uint8,device=x.device)
|
| 1233 |
+
for i in range(0,h-38,crop_size[0]):
|
| 1234 |
+
for j in range(0,w-38,crop_size[1]):
|
| 1235 |
+
tmp0,x_crop = self.unet1.forward_a(x[:,:,i:i+crop_size[0]+38,j:j+crop_size[1]+38])
|
| 1236 |
+
x_crop=self.unet1.conv2.seblock.forward_mean(x_crop,se_mean0/n_patch)
|
| 1237 |
+
x_crop=self.unet1.forward_b(tmp0,x_crop)
|
| 1238 |
+
tmp_x1,tmp_x2 = self.unet2.forward_a(x_crop)
|
| 1239 |
+
x_crop = F.pad(x_crop,(-20,-20,-20,-20))
|
| 1240 |
+
tmp_x2=self.unet2.conv2.seblock.forward_mean(tmp_x2,se_mean1/n_patch)
|
| 1241 |
+
tmp_x2, tmp_x3 = self.unet2.forward_b(tmp_x2)
|
| 1242 |
+
tmp_x3=self.unet2.conv3.seblock.forward_mean(tmp_x3,se_mean2/n_patch)
|
| 1243 |
+
tmp_x4=self.unet2.forward_c(tmp_x2,tmp_x3)*alpha
|
| 1244 |
+
tmp_x4=self.unet2.conv4.seblock.forward_mean(tmp_x4,se_mean3/n_patch)
|
| 1245 |
+
x0=self.unet2.forward_d(tmp_x1,tmp_x4)
|
| 1246 |
+
x_crop = torch.add(x0, x_crop)
|
| 1247 |
+
x_crop=self.conv_final(x_crop)
|
| 1248 |
+
x_crop = F.pad(x_crop, (-1, -1, -1, -1))
|
| 1249 |
+
x_crop=self.ps(x_crop)
|
| 1250 |
+
x00_crop=x00[:, :, i:i + h1 - 38, j:j + w1 - 38]
|
| 1251 |
+
_,_,h2,w2=x00_crop.shape
|
| 1252 |
+
x_crop[:,:,:h2*4,:w2*4]+=F.interpolate(x00_crop, scale_factor=4, mode='nearest')
|
| 1253 |
+
if(pro):
|
| 1254 |
+
res[:, :, i * 4:i * 4 + h1 * 4 - 152, j * 4:j * 4 + w1 * 4 - 152] = ((x_crop-0.15) * (255/0.7)).round().clamp_(0, 255).byte()
|
| 1255 |
+
else:
|
| 1256 |
+
res[:, :, i * 4:i * 4 + h1 * 4 - 152, j * 4:j * 4 + w1 * 4 - 152] = (x_crop*255).round().clamp_(0, 255).byte()
|
| 1257 |
+
#torch.cuda.empty_cache()
|
| 1258 |
+
if(w0!=pw or h0!=ph):res=res[:,:,:h0*4,:w0*4]
|
| 1259 |
+
return res
|
| 1260 |
+
class RealWaifuUpScaler(object):
|
| 1261 |
+
def __init__(self,scale,weight_path,half,device):
|
| 1262 |
+
weight = torch.load(weight_path, map_location="cpu")
|
| 1263 |
+
self.pro="pro"in weight
|
| 1264 |
+
if(self.pro):del weight["pro"]
|
| 1265 |
+
self.model=eval("UpCunet%sx"%scale)()
|
| 1266 |
+
if(half==True):self.model=self.model.half().to(device)
|
| 1267 |
+
else:self.model=self.model.to(device)
|
| 1268 |
+
self.model.load_state_dict(weight, strict=True)
|
| 1269 |
+
self.model.eval()
|
| 1270 |
+
self.half=half
|
| 1271 |
+
self.device=device
|
| 1272 |
+
|
| 1273 |
+
def np2tensor(self,np_frame):
|
| 1274 |
+
if(self.pro):
|
| 1275 |
+
if (self.half == False):return torch.from_numpy(np.transpose(np_frame, (2, 0, 1))).unsqueeze(0).to(self.device).float() / (255/0.7)+0.15
|
| 1276 |
+
else:return torch.from_numpy(np.transpose(np_frame, (2, 0, 1))).unsqueeze(0).to(self.device).half() / (255/0.7)+0.15
|
| 1277 |
+
else:
|
| 1278 |
+
if (self.half == False):return torch.from_numpy(np.transpose(np_frame, (2, 0, 1))).unsqueeze(0).to(self.device).float() / 255
|
| 1279 |
+
else:return torch.from_numpy(np.transpose(np_frame, (2, 0, 1))).unsqueeze(0).to(self.device).half() / 255
|
| 1280 |
+
|
| 1281 |
+
def tensor2np(self,tensor):
|
| 1282 |
+
return (np.transpose(tensor.squeeze().cpu().numpy(), (1, 2, 0)))
|
| 1283 |
+
|
| 1284 |
+
def __call__(self, frame,tile_mode,cache_mode,alpha):
|
| 1285 |
+
with torch.no_grad():
|
| 1286 |
+
tensor = self.np2tensor(frame)
|
| 1287 |
+
if(cache_mode==3):
|
| 1288 |
+
result = self.tensor2np(self.model.forward_gap_sync(tensor,tile_mode,alpha,self.pro))
|
| 1289 |
+
elif(cache_mode==2):
|
| 1290 |
+
result = self.tensor2np(self.model.forward_fast_rough(tensor,tile_mode,alpha,self.pro))
|
| 1291 |
+
else:
|
| 1292 |
+
result = self.tensor2np(self.model(tensor,tile_mode,cache_mode,alpha,self.pro))
|
| 1293 |
+
return result
|
| 1294 |
+
|
| 1295 |
+
if __name__ == "__main__":
|
| 1296 |
+
###########inference_img
|
| 1297 |
+
import time, cv2,sys,pdb
|
| 1298 |
+
from time import time as ttime
|
| 1299 |
+
for weight_path, scale in [("weights_v3/up2x-latest-denoise3x.pth", 2),("weights_v3/up3x-latest-denoise3x.pth", 3),("weights_v3/up4x-latest-denoise3x.pth", 4),("weights_pro/pro-denoise3x-up2x.pth", 2),("weights_pro/pro-denoise3x-up3x.pth", 3),]:
|
| 1300 |
+
for tile_mode in [0,5]:
|
| 1301 |
+
for cache_mode in [0,1,2,3]:
|
| 1302 |
+
for alpha in [1]:
|
| 1303 |
+
weight_name=weight_path.split("/")[-1].split(".")[0]
|
| 1304 |
+
upscaler2x = RealWaifuUpScaler(scale, weight_path, half=True, device="cuda:0")
|
| 1305 |
+
input_dir="%s/inputs"%root_path
|
| 1306 |
+
output_dir="%s/output-dir-all-test"%root_path
|
| 1307 |
+
os.makedirs(output_dir,exist_ok=True)
|
| 1308 |
+
for name in os.listdir(input_dir):
|
| 1309 |
+
print(name)
|
| 1310 |
+
tmp = name.split(".")
|
| 1311 |
+
inp_path = os.path.join(input_dir, name)
|
| 1312 |
+
suffix = tmp[-1]
|
| 1313 |
+
prefix = ".".join(tmp[:-1])
|
| 1314 |
+
tmp_path = os.path.join(root_path, "tmp", "%s.%s" % (int(time.time() * 1000000), suffix))
|
| 1315 |
+
print(inp_path,tmp_path)
|
| 1316 |
+
#支持中文路径
|
| 1317 |
+
#os.link(inp_path, tmp_path)#win用硬链接
|
| 1318 |
+
os.symlink(inp_path, tmp_path)#linux用软链接
|
| 1319 |
+
frame = cv2.imread(tmp_path)[:, :, [2, 1, 0]]
|
| 1320 |
+
t0 = ttime()
|
| 1321 |
+
result = upscaler2x(frame, tile_mode=tile_mode,cache_mode=cache_mode,alpha=alpha)[:, :, ::-1]
|
| 1322 |
+
t1 = ttime()
|
| 1323 |
+
print(prefix, "done", t1 - t0,"tile_mode=%s"%tile_mode,cache_mode)
|
| 1324 |
+
tmp_opt_path = os.path.join(root_path, "tmp", "%s.%s" % (int(time.time() * 1000000), suffix))
|
| 1325 |
+
cv2.imwrite(tmp_opt_path, result)
|
| 1326 |
+
n=0
|
| 1327 |
+
while (1):
|
| 1328 |
+
if (n == 0):suffix = "_%sx_tile%s_cache%s_alpha%s_%s.png" % (scale, tile_mode, cache_mode, alpha,weight_name)
|
| 1329 |
+
else:suffix = "_%sx_tile%s_cache%s_alpha%s_%s_%s.png" % (scale, tile_mode, cache_mode, alpha, weight_name,n)
|
| 1330 |
+
if (os.path.exists(os.path.join(output_dir, prefix + suffix)) == False):break
|
| 1331 |
+
else:n += 1
|
| 1332 |
+
final_opt_path=os.path.join(output_dir, prefix + suffix)
|
| 1333 |
+
os.rename(tmp_opt_path,final_opt_path)
|
| 1334 |
+
os.remove(tmp_path)
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.12
|
| 2 |
+
uvicorn[standard]==0.30.6
|
| 3 |
+
python-multipart==0.0.9
|
| 4 |
+
Jinja2==3.1.4
|
| 5 |
+
itsdangerous==2.2.0
|
| 6 |
+
requests==2.32.3
|
| 7 |
+
Pillow==10.4.0
|
| 8 |
+
numpy==1.26.4
|
weights/up2x-latest-conservative.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6cfe3b23687915d08ba96010f25198d9cfe8a683aa4131f1acf7eaa58ee1de93
|
| 3 |
+
size 5147249
|
weights/up2x-latest-denoise1x.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2e783c39da6a6394fbc250fdd069c55eaedc43971c4f2405322f18949ce38573
|
| 3 |
+
size 5147249
|
weights/up2x-latest-denoise2x.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8188b3faef4258cf748c59360cbc8086ebedf4a63eb9d5d6637d45f819d32496
|
| 3 |
+
size 5147249
|
weights/up2x-latest-denoise3x.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0a14739f3f5fcbd74ec3ce2806d13a47916c916b20afe4a39d95f6df4ca6abd8
|
| 3 |
+
size 5147249
|
weights/up2x-latest-no-denoise.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f491f9ecf6964ead9f3a36bf03e83527f32c6a341b683f7378ac6c1e2a5f0d16
|
| 3 |
+
size 5147249
|
weights/up3x-latest-conservative.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f6ea5fd20380413beb2701182483fd80c2e86f3b3f08053eb3df4975184aefe3
|
| 3 |
+
size 5154161
|
weights/up3x-latest-denoise3x.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:39f1e6e90d50e5528a63f4ba1866bad23365a737cbea22a80769b2ec4c1c3285
|
| 3 |
+
size 5154161
|
weights/up3x-latest-no-denoise.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:763f0a87e70d744673f1a41db5396d5f334d22de97fff68ffc40deb91404a584
|
| 3 |
+
size 5154161
|
weights/up4x-latest-conservative.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a8c8185def699b0883662a02df0ef2e6db3b0275170b6cc0d28089b64b273427
|
| 3 |
+
size 5636403
|
weights/up4x-latest-denoise3x.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:42bd8fcdae37c12c5b25ed59625266bfa65780071a8d38192d83756cb85e98dd
|
| 3 |
+
size 5636403
|
weights/up4x-latest-no-denoise.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aaf3ef78a488cce5d3842154925eb70ff8423b8298e2cd189ec66eb7f6f66fae
|
| 3 |
+
size 5636403
|