huanx commited on
Commit
8a245da
·
verified ·
1 Parent(s): f761d21

Deploy Real-CUGAN control plane with bundled weights

Browse files
.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 Cugan Hf Api
3
- emoji: 🌖
4
- colorFrom: pink
5
- colorTo: purple
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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