krau commited on
Commit
7d54ba7
·
unverified ·
1 Parent(s): 68dee39

init commit

Browse files
Files changed (16) hide show
  1. .env.example +19 -0
  2. .gitignore +8 -0
  3. .python-version +1 -0
  4. Dockerfile +5 -0
  5. README.md +1 -0
  6. common.py +242 -0
  7. config.py +9 -0
  8. logger.py +8 -0
  9. main.py +417 -0
  10. onnx_infer.py +243 -0
  11. pyproject.toml +24 -0
  12. requirements.txt +38 -0
  13. settings.toml +19 -0
  14. sr_queue.py +288 -0
  15. test_common.py +115 -0
  16. uv.lock +576 -0
.env.example ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SRAPI_PROVIDER = "CPUExecutionProvider"
2
+ SRAPI_LOG_LEVEL = "INFO"
3
+ SRAPI_TOKEN = "token"
4
+ SRAPI_REDIS_URL = "redis://localhost:6379"
5
+ SRAPI_HOST = "0.0.0.0"
6
+ SRAPI_PORT = 39721
7
+ SRAPI_TEMP_DIR = "./temp"
8
+ SRAPI_OUTPUT_DIR = "./output"
9
+ SRAPI_MAX_TIMEOUT = 300
10
+ SRAPI_TIMEOUT = 30
11
+ SRAPI_MAX_THREAD = 8
12
+ SRAPI_MODE = "master"
13
+ SRAPI_MASTER_URL = "http://localhost:39721"
14
+ SRAPI_MASTER_TOKEN = "token"
15
+ SRAPI_WORKER_ID = "special_id"
16
+ SRAPI_WORKER_URL = "http://localhost:39721"
17
+ SRAPI_WORKER_CHECK_INTERVAL = 5
18
+ SRAPI_WORKER_EXPIRE = 120
19
+ SRAPI_REGISTER_INTERVAL = 30
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ venv/
2
+ output*/
3
+ __pycache__/
4
+ # Ignore dynaconf secret files
5
+ .secrets.*
6
+ temp*/
7
+ .env
8
+ models/
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
Dockerfile ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim
2
+ COPY . /sr_api
3
+ WORKDIR /sr_api
4
+ RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y && uv sync --frozen --no-dev
5
+ ENTRYPOINT [ "uv" ,"run", "python", "main.py" ]
README.md CHANGED
@@ -6,6 +6,7 @@ colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
  license: gpl-3.0
 
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
6
  sdk: docker
7
  pinned: false
8
  license: gpl-3.0
9
+ app_port: 39721
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
common.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pathlib
2
+ from dataclasses import dataclass
3
+
4
+ import cv2
5
+ import numpy as np
6
+ import redis
7
+ from logger import logger
8
+
9
+ from config import settings
10
+
11
+ try:
12
+ logger.info(
13
+ f"Connecting to Redis: {settings.get('redis_url', 'redis://localhost:6379')}"
14
+ )
15
+ redis_client = redis.from_url(settings.get("redis_url", "redis://localhost:6379"))
16
+ logger.info(f"Redis ping: {redis_client.ping()}")
17
+ except Exception as e:
18
+ logger.error(f"Failed to connect to Redis: {e}")
19
+ exit(1)
20
+
21
+
22
+ @dataclass
23
+ class ModelInfo:
24
+ name: str = ""
25
+ path: str = ""
26
+ scale: int = 4
27
+ algo: str = ""
28
+
29
+
30
+ BASE_STREAM_NAME = (
31
+ "super_resolution_api_queue"
32
+ if not settings.get("worker_id")
33
+ else f"super_resolution_api_queue_{settings.get('worker_id')}"
34
+ )
35
+ WORKER_KEY_PREFIX = "super_resolution_api_worker_"
36
+ DISTRIBUTED_STREAM_NAME = "super_resolution_api_distributed_queue"
37
+ RESULT_KEY_PREFIX = (
38
+ "super_resolution_api_result_"
39
+ if not settings.get("worker_id")
40
+ else f"super_resolution_api_result_{settings.get('worker_id')}_"
41
+ )
42
+ PROGRESS_TIMEOUT = settings.get("timeout", 30)
43
+ MAX_ALLOWED_TIMEOUT = settings.get("max_timeout", 300)
44
+ MAX_THREAD = settings.get("max_thread", 8)
45
+ MODEL_NAME_DEFAULT = "x4_Anime_6B-Official"
46
+ MODEL_NAME_X4_JP_ILLUSTRATION_FIX1 = "x4_JP_Illustration-fix1"
47
+ MODEL_NAME_X4_JP_ILLUSTRATION_FIX2 = "x4_JP_Illustration-fix2"
48
+ MODEL_NAME_X4_JP_ILLUSTRATION_FIX1_D = "x4_JP_Illustration-fix1-d"
49
+ MODEL_NAME_X4_ANIME_6B_OFFICIAL = "x4_Anime_6B-Official"
50
+
51
+
52
+ model_Anime_Official = ModelInfo(
53
+ MODEL_NAME_X4_ANIME_6B_OFFICIAL,
54
+ "models/x4_Anime_6B-Official.onnx",
55
+ 4,
56
+ "real-esrgan",
57
+ )
58
+
59
+ model_JP_Illustration_fix1 = ModelInfo(
60
+ MODEL_NAME_X4_JP_ILLUSTRATION_FIX1,
61
+ "models/x4_jp_Illustration-fix1.onnx",
62
+ 4,
63
+ "real-hatgan",
64
+ )
65
+
66
+
67
+ model_JP_Illustration_fix2 = ModelInfo(
68
+ MODEL_NAME_X4_JP_ILLUSTRATION_FIX2,
69
+ "models/x4_jp_Illustration-fix2.onnx",
70
+ 4,
71
+ "real-esrgan",
72
+ )
73
+
74
+ model_JP_Illustration_fix1_d = ModelInfo(
75
+ MODEL_NAME_X4_JP_ILLUSTRATION_FIX1_D,
76
+ "models/x4_jp_Illustration-fix1-d.onnx",
77
+ 4,
78
+ "real-esrgan",
79
+ )
80
+
81
+ models = {
82
+ MODEL_NAME_X4_ANIME_6B_OFFICIAL: model_Anime_Official,
83
+ MODEL_NAME_X4_JP_ILLUSTRATION_FIX1: model_JP_Illustration_fix1,
84
+ MODEL_NAME_X4_JP_ILLUSTRATION_FIX2: model_JP_Illustration_fix2,
85
+ MODEL_NAME_X4_JP_ILLUSTRATION_FIX1_D: model_JP_Illustration_fix1_d,
86
+ }
87
+
88
+
89
+ def get_image_size(image_path: pathlib.Path) -> tuple[int, int]:
90
+ """
91
+ return: (width, height)
92
+ """
93
+ img = cv2.imread(str(image_path))
94
+ if img is None:
95
+ raise Exception(f"Failed to load image: {image_path}")
96
+ return img.shape[1], img.shape[0]
97
+
98
+
99
+ @dataclass
100
+ class TileInfo:
101
+ x: int
102
+ y: int
103
+ filpath: pathlib.Path
104
+
105
+
106
+ def split_image(
107
+ img_path: pathlib.Path,
108
+ save_dir: pathlib.Path,
109
+ grid_size: tuple[int, int],
110
+ overlap: int = 16,
111
+ ) -> list[TileInfo]:
112
+ save_path = pathlib.Path(save_dir)
113
+ save_path.mkdir(parents=True, exist_ok=True)
114
+
115
+ img = cv2.imread(str(img_path))
116
+ if img is None:
117
+ raise Exception(f"Failed to load image: {img_path}")
118
+
119
+ height, width = img.shape[:2]
120
+ rows, cols = grid_size
121
+
122
+ base_h = height // rows
123
+ base_w = width // cols
124
+
125
+ tiles_info = []
126
+
127
+ for row in range(rows):
128
+ for col in range(cols):
129
+ x1 = max(0, col * base_w - overlap)
130
+ y1 = max(0, row * base_h - overlap)
131
+ x2 = min(width, (col + 1) * base_w + overlap)
132
+ y2 = min(height, (row + 1) * base_h + overlap)
133
+
134
+ tile = img[y1:y2, x1:x2]
135
+ tile_name = f"{img_path.stem}_tile_{row}_{col}.png"
136
+ tile_path = save_path / tile_name
137
+ cv2.imwrite(str(tile_path), tile)
138
+ tiles_info.append(TileInfo(col, row, tile_path))
139
+
140
+ return tiles_info
141
+
142
+
143
+ def merge_sr_tiles(
144
+ tiles: list[TileInfo],
145
+ output: pathlib.Path,
146
+ original_size: tuple[int, int],
147
+ scale: int,
148
+ overlap: int = 16,
149
+ ):
150
+ """
151
+ 合并超分辨率后的图块
152
+
153
+ tiles: 超分辨率后的图块信息列表, 需要根据 filepath 读取图块, 根据 x, y 位置信息进行拼接
154
+ output: 合并后的图片保存路径
155
+ original_size: 原始图片的尺寸
156
+ overlap 为原始图片切割时设定的重叠像素数
157
+ scale 为超分辨率倍数
158
+ """
159
+ # Calculate output dimensions
160
+ logger.debug(
161
+ f"正在合并 {len(tiles)} 张超分辨率图块, 原尺寸: {original_size}, 缩放倍数: {scale}"
162
+ )
163
+
164
+ width, height = original_size
165
+ out_width = width * scale
166
+ out_height = height * scale
167
+ output_img = np.zeros((out_height, out_width, 3), dtype=np.uint8)
168
+
169
+ # Calculate base tile sizes
170
+ rows = max([t.y for t in tiles]) + 1
171
+ cols = max([t.x for t in tiles]) + 1
172
+ base_h = height // rows
173
+ base_w = width // cols
174
+
175
+ # Scale dimensions
176
+ scaled_base_h = base_h * scale
177
+ scaled_base_w = base_w * scale
178
+ scaled_overlap = overlap * scale
179
+
180
+ for tile_info in tiles:
181
+ # Read tile
182
+ tile = cv2.imread(str(tile_info.filpath))
183
+ if tile is None:
184
+ raise Exception(f"Failed to load tile: {tile_info.filpath}")
185
+
186
+ # Calculate positions
187
+ x1 = max(0, tile_info.x * scaled_base_w - scaled_overlap)
188
+ y1 = max(0, tile_info.y * scaled_base_h - scaled_overlap)
189
+ x2 = min(out_width, (tile_info.x + 1) * scaled_base_w + scaled_overlap)
190
+ y2 = min(out_height, (tile_info.y + 1) * scaled_base_h + scaled_overlap)
191
+
192
+ # Calculate blend mask for overlapping regions
193
+ h, w = y2 - y1, x2 - x1
194
+ blend_mask = np.ones((h, w, 1), dtype=np.float32)
195
+
196
+ # Apply feathering at edges
197
+ if tile_info.x > 0: # Left edge
198
+ blend_mask[:, :scaled_overlap] = np.linspace(0, 1, scaled_overlap).reshape(
199
+ 1, -1, 1
200
+ )
201
+ if tile_info.x < cols - 1: # Right edge
202
+ blend_mask[:, -scaled_overlap:] = np.linspace(1, 0, scaled_overlap).reshape(
203
+ 1, -1, 1
204
+ )
205
+ if tile_info.y > 0: # Top edge
206
+ blend_mask[:scaled_overlap, :] *= np.linspace(0, 1, scaled_overlap).reshape(
207
+ -1, 1, 1
208
+ )
209
+ if tile_info.y < rows - 1: # Bottom edge
210
+ blend_mask[-scaled_overlap:, :] *= np.linspace(
211
+ 1, 0, scaled_overlap
212
+ ).reshape(-1, 1, 1)
213
+
214
+ # Blend tiles
215
+ output_img[y1:y2, x1:x2] = (
216
+ output_img[y1:y2, x1:x2] * (1 - blend_mask)
217
+ + tile[: y2 - y1, : x2 - x1] * blend_mask
218
+ ).astype(np.uint8)
219
+
220
+ cv2.imwrite(output.as_posix(), output_img)
221
+
222
+
223
+ def calculate_grid(image_width, image_height, workers):
224
+ if workers <= 0:
225
+ raise ValueError("Worker count must be positive")
226
+
227
+ best_rows, best_cols = 1, workers
228
+ min_aspect_diff = float("inf")
229
+
230
+ for rows in range(1, workers + 1):
231
+ if workers % rows == 0:
232
+ cols = workers // rows
233
+ tile_width = image_width / cols
234
+ tile_height = image_height / rows
235
+ aspect_ratio = max(tile_width, tile_height) / min(tile_width, tile_height)
236
+ aspect_diff = aspect_ratio - 1
237
+
238
+ if aspect_diff < min_aspect_diff:
239
+ best_rows, best_cols = rows, cols
240
+ min_aspect_diff = aspect_diff
241
+ logger.debug(f"calculate_grid: {best_rows}x{best_cols}")
242
+ return best_rows, best_cols
config.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from dynaconf import Dynaconf
2
+
3
+ settings = Dynaconf(
4
+ envvar_prefix="SRAPI",
5
+ settings_files=["settings.toml", ".secrets.toml"],
6
+ )
7
+
8
+ # `envvar_prefix` = export envvars with `export DYNACONF_FOO=bar`.
9
+ # `settings_files` = Load these files in the order.
logger.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from loguru import logger
3
+
4
+ import config
5
+
6
+ logger.remove()
7
+
8
+ logger.add(sys.stdout, level=config.settings.get("log_level", "INFO"))
main.py ADDED
@@ -0,0 +1,417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pathlib
2
+ import pickle
3
+ import shutil
4
+ import tempfile
5
+ import threading
6
+ import time
7
+
8
+ import httpx
9
+ from fastapi import (
10
+ Depends,
11
+ FastAPI,
12
+ File,
13
+ Form,
14
+ Header,
15
+ HTTPException,
16
+ UploadFile,
17
+ status,
18
+ )
19
+ from fastapi.middleware.cors import CORSMiddleware
20
+ from fastapi.responses import FileResponse
21
+ from logger import logger
22
+
23
+ import common
24
+ from config import settings
25
+ from sr_queue import listen_distributed_queue, listen_queue
26
+
27
+
28
+ async def verify_token(x_token: str = Header()):
29
+ if x_token != settings.get("token"):
30
+ raise HTTPException(
31
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid X-Token"
32
+ )
33
+
34
+
35
+ app = FastAPI(
36
+ dependencies=[Depends(verify_token)],
37
+ title="Super Resolution API",
38
+ description="Super Resolution API for Anime and Illustration",
39
+ )
40
+
41
+ app.add_middleware(
42
+ CORSMiddleware,
43
+ allow_origins=["*"],
44
+ allow_credentials=True,
45
+ allow_methods=["*"],
46
+ allow_headers=["*"],
47
+ )
48
+
49
+
50
+ def register_routes():
51
+ @app.get("/")
52
+ async def root():
53
+ return {
54
+ "message": f"Super Resolution API is running as {settings.get('mode', 'single')} mode"
55
+ }
56
+
57
+ @app.get("/result/{task_id}")
58
+ async def get_result(task_id: str):
59
+ result = common.redis_client.get(f"{common.RESULT_KEY_PREFIX}{task_id}")
60
+ if result is None:
61
+ raise HTTPException(
62
+ status_code=status.HTTP_404_NOT_FOUND, detail="Task not found"
63
+ )
64
+ result_data: dict[str, str] = pickle.loads(result)
65
+ return {"result": result_data}
66
+
67
+ @app.get("/result/{task_id}/download")
68
+ async def download_result(task_id: str):
69
+ result = common.redis_client.get(f"{common.RESULT_KEY_PREFIX}{task_id}")
70
+ if result is None:
71
+ raise HTTPException(
72
+ status_code=status.HTTP_404_NOT_FOUND, detail="Task not found"
73
+ )
74
+ result_data: dict[str, str] = pickle.loads(result)
75
+ if result_data["status"] != "success":
76
+ raise HTTPException(
77
+ status_code=status.HTTP_400_BAD_REQUEST,
78
+ detail=f"Task is {result_data['status']}",
79
+ )
80
+ file_path = pathlib.Path(result_data["path"])
81
+ if not file_path.exists():
82
+ raise HTTPException(
83
+ status_code=status.HTTP_404_NOT_FOUND, detail="File not found"
84
+ )
85
+ return FileResponse(
86
+ path=file_path,
87
+ filename=file_path.name,
88
+ headers={"Content-Length": str(file_path.stat().st_size)},
89
+ media_type="image/png",
90
+ )
91
+
92
+
93
+ def register_single_sr_route():
94
+ @app.post("/sr")
95
+ async def super_resolution(
96
+ file: UploadFile | None = File(default=None),
97
+ tile_size: int = Form(default=64, ge=32, le=128),
98
+ scale: int = Form(default=4, ge=2, le=8),
99
+ skip_alpha: bool = Form(default=False),
100
+ resize_to: str | None = Form(default=None),
101
+ url: str | None = Form(default=None),
102
+ timeout: int = Form(
103
+ default=common.PROGRESS_TIMEOUT, ge=1, le=common.MAX_ALLOWED_TIMEOUT
104
+ ),
105
+ model: str = Form(default=common.MODEL_NAME_DEFAULT),
106
+ ):
107
+ if (file or url) is None:
108
+ raise HTTPException(
109
+ status_code=status.HTTP_400_BAD_REQUEST,
110
+ detail="No file or url provided",
111
+ )
112
+ temp = tempfile.NamedTemporaryFile(
113
+ dir=settings.get("temp_dir", "./temp"), delete=False
114
+ )
115
+ temp_path = pathlib.Path(temp.name)
116
+ try:
117
+ if url is not None:
118
+ async with httpx.AsyncClient() as client:
119
+ response = await client.get(url)
120
+ if response.status_code != 200:
121
+ return {"message": "Failed to download the image"}
122
+ if response.headers.get("Content-Type") not in [
123
+ "image/jpeg",
124
+ "image/png",
125
+ "image/webp",
126
+ ]:
127
+ raise HTTPException(
128
+ status_code=status.HTTP_400_BAD_REQUEST,
129
+ detail="Invalid image format",
130
+ )
131
+ temp.write(response.content)
132
+ else:
133
+ if file.content_type not in ["image/jpeg", "image/png", "image/webp"]:
134
+ raise HTTPException(
135
+ status_code=status.HTTP_400_BAD_REQUEST,
136
+ detail="Invalid image format",
137
+ )
138
+ temp.write(file.file.read())
139
+ except Exception as e:
140
+ logger.error(f"process image error: {e}")
141
+ temp.close()
142
+ raise HTTPException(
143
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
144
+ detail="Failed to process the image",
145
+ )
146
+ resp = common.redis_client.xadd(
147
+ common.BASE_STREAM_NAME,
148
+ {
149
+ "data": pickle.dumps(
150
+ {
151
+ "input_image": temp_path,
152
+ "tile_size": tile_size,
153
+ "scale": scale,
154
+ "skip_alpha": skip_alpha,
155
+ "resize_to": resize_to,
156
+ "timeout": timeout,
157
+ "model": model,
158
+ }
159
+ ),
160
+ },
161
+ )
162
+ xlength = common.redis_client.xlen(common.BASE_STREAM_NAME)
163
+ if xlength > 1:
164
+ common.redis_client.set(
165
+ f"{common.RESULT_KEY_PREFIX}{resp.decode('utf-8')}",
166
+ pickle.dumps({"status": "pending"}),
167
+ ex=86400,
168
+ )
169
+ logger.info(f"Task added to queue: {resp.decode('utf-8')}")
170
+ return {"message": "Success", "task_id": f"{resp.decode('utf-8')}"}
171
+
172
+
173
+ def register_master():
174
+ @app.post("/register")
175
+ async def register_worker(
176
+ worker_id: str = Form(...),
177
+ worker_url: str = Form(...),
178
+ worker_token: str = Form(...),
179
+ ):
180
+ try:
181
+ common.redis_client.set(
182
+ f"{common.WORKER_KEY_PREFIX}{worker_id}",
183
+ f"{worker_url}|{worker_token}",
184
+ ex=settings.get("worker_expire", 120),
185
+ )
186
+ except Exception as e:
187
+ logger.error(f"Failed to register worker: {e}")
188
+ raise HTTPException(
189
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
190
+ detail="Failed to register worker",
191
+ )
192
+ return {"message": "Success"}
193
+
194
+ @app.get("/workers")
195
+ async def get_workers():
196
+ workers = common.redis_client.keys(f"{common.WORKER_KEY_PREFIX}*")
197
+ return {
198
+ "workers": [
199
+ {
200
+ "id": worker.decode("utf-8").split("_")[-1],
201
+ "data": common.redis_client.get(worker).decode("utf-8"),
202
+ }
203
+ for worker in workers
204
+ ]
205
+ }
206
+
207
+ @app.post("/sr")
208
+ async def super_resolution(
209
+ file: UploadFile | None = File(default=None),
210
+ tile_size: int = Form(default=64, ge=32, le=128),
211
+ scale: int = Form(default=4, ge=2, le=8),
212
+ skip_alpha: bool = Form(default=False),
213
+ resize_to: str | None = Form(default=None),
214
+ url: str | None = Form(default=None),
215
+ timeout: int = Form(
216
+ default=common.PROGRESS_TIMEOUT, ge=1, le=common.MAX_ALLOWED_TIMEOUT
217
+ ),
218
+ model: str = Form(default=common.MODEL_NAME_DEFAULT),
219
+ ):
220
+ """
221
+ 将输入图片分块, 分发给存储在 Redis 中的 worker
222
+
223
+ 对于客户端来说, 该 /sr 路由和 single 模式是兼容的
224
+ """
225
+ if (file or url) is None:
226
+ raise HTTPException(
227
+ status_code=status.HTTP_400_BAD_REQUEST,
228
+ detail="No file or url provided",
229
+ )
230
+
231
+ workers = common.redis_client.keys(f"{common.WORKER_KEY_PREFIX}*")
232
+ if not workers:
233
+ raise HTTPException(
234
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
235
+ detail="No available worker",
236
+ )
237
+
238
+ input_temp = tempfile.NamedTemporaryFile(
239
+ dir=settings.get("temp_dir", "./temp"), delete=False
240
+ )
241
+ input_path = pathlib.Path(input_temp.name)
242
+ try:
243
+ if url is not None:
244
+ async with httpx.AsyncClient() as client:
245
+ response = await client.get(url)
246
+ if response.status_code != 200:
247
+ return {"message": "Failed to download the image"}
248
+ if response.headers.get("Content-Type") not in [
249
+ "image/jpeg",
250
+ "image/png",
251
+ "image/webp",
252
+ ]:
253
+ raise HTTPException(
254
+ status_code=status.HTTP_400_BAD_REQUEST,
255
+ detail="Invalid image format",
256
+ )
257
+ input_temp.write(response.content)
258
+ else:
259
+ if file.content_type not in ["image/jpeg", "image/png", "image/webp"]:
260
+ raise HTTPException(
261
+ status_code=status.HTTP_400_BAD_REQUEST,
262
+ detail="Invalid image format",
263
+ )
264
+ input_temp.write(file.file.read())
265
+ except Exception as e:
266
+ logger.error(f"process image error: {e}")
267
+ input_temp.close()
268
+ raise HTTPException(
269
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
270
+ detail="Failed to process the image",
271
+ )
272
+
273
+ workers = common.redis_client.keys(f"{common.WORKER_KEY_PREFIX}*")
274
+ if not workers:
275
+ raise HTTPException(
276
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
277
+ detail="No available worker",
278
+ )
279
+ try:
280
+ save_dir = pathlib.Path(
281
+ f"{settings.get('output_dir','./output')}/{input_temp.name.split('/')[-1]}"
282
+ )
283
+ origin_width, origin_height = common.get_image_size(input_path)
284
+ origin_tiles_info = common.split_image(
285
+ input_path,
286
+ save_dir,
287
+ common.calculate_grid(origin_width, origin_height, len(workers)),
288
+ )
289
+
290
+ response = {}
291
+
292
+ for index, worker_key in enumerate(workers):
293
+ worker = common.redis_client.get(worker_key)
294
+ worker_url, token = worker.decode("utf-8").split("|")
295
+ tile_info = origin_tiles_info[index]
296
+
297
+ with open(tile_info.filpath, "rb") as tile_file:
298
+ async with httpx.AsyncClient() as client:
299
+ resp = await client.get(
300
+ worker_url + "/", headers={"X-Token": token}
301
+ )
302
+ if resp.status_code != 200:
303
+ raise Exception(f"Worker {worker_url} is not available")
304
+ resp = await client.post(
305
+ url=f"{worker_url}/sr",
306
+ files={"file": tile_file},
307
+ data={
308
+ "tile_size": tile_size,
309
+ "scale": scale,
310
+ "skip_alpha": skip_alpha,
311
+ "resize_to": resize_to,
312
+ "timeout": timeout,
313
+ "model": model,
314
+ },
315
+ headers={"X-Token": token},
316
+ )
317
+ if resp.status_code != 200:
318
+ raise Exception(
319
+ f"Woker {worker_url} failed to process the image: {resp.text}"
320
+ )
321
+
322
+ resp_dict = resp.json().copy()
323
+ resp_dict["tile_info"] = tile_info
324
+ response[worker_key] = resp_dict
325
+
326
+ except Exception as e:
327
+ logger.error(f"error: {e}")
328
+ input_temp.close()
329
+ raise HTTPException(
330
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
331
+ detail=f"Failed to process the image: {e}",
332
+ )
333
+
334
+ resp = common.redis_client.xadd(
335
+ common.DISTRIBUTED_STREAM_NAME,
336
+ {
337
+ "data": pickle.dumps(
338
+ {
339
+ "input_image": input_path,
340
+ "worker_response": response,
341
+ "scale": scale,
342
+ }
343
+ )
344
+ },
345
+ )
346
+ common.redis_client.set(
347
+ f"{common.RESULT_KEY_PREFIX}{resp.decode('utf-8')}",
348
+ pickle.dumps({"status": "pending"}),
349
+ ex=86400,
350
+ )
351
+
352
+ return {"message": "Success", "task_id": f"{resp.decode('utf-8')}"}
353
+
354
+
355
+ def register_slave():
356
+ register_single_sr_route()
357
+
358
+ def register():
359
+ while True:
360
+ try:
361
+ with httpx.Client() as client:
362
+ resp = client.post(
363
+ url=f"{settings.get('master_url')}/register",
364
+ data={
365
+ "worker_id": settings.get("worker_id"),
366
+ "worker_url": settings.get("worker_url"),
367
+ "worker_token": settings.get("token"),
368
+ },
369
+ headers={"X-Token": settings.get("master_token")},
370
+ )
371
+ if resp.status_code != 200:
372
+ logger.error(f"Failed to register to master: {resp.text}")
373
+ except Exception as e:
374
+ logger.error(f"Registration error: {e}")
375
+ finally:
376
+ time.sleep(settings.get("register_interval", 30))
377
+
378
+ register_thread = threading.Thread(target=register)
379
+ register_thread.daemon = True
380
+ register_thread.start()
381
+
382
+
383
+ if __name__ == "__main__":
384
+ register_routes()
385
+ if settings.get("mode", "single") == "single":
386
+ register_single_sr_route()
387
+ elif settings.get("mode") == "master":
388
+ register_master()
389
+ queue_thread = threading.Thread(target=listen_distributed_queue)
390
+ queue_thread.daemon = True
391
+ queue_thread.start()
392
+ else:
393
+ register_slave()
394
+
395
+ if settings.get("mode") != "master":
396
+ queue_thread = threading.Thread(target=listen_queue)
397
+ queue_thread.daemon = True
398
+ queue_thread.start()
399
+
400
+ if not pathlib.Path(settings.get("temp_dir", "./temp")).exists():
401
+ pathlib.Path(settings.get("temp_dir", "./temp")).mkdir(parents=True)
402
+ import uvicorn
403
+
404
+ try:
405
+ uvicorn.run(
406
+ app,
407
+ host=settings.get("host", "0.0.0.0"),
408
+ port=settings.get("port", 39721),
409
+ )
410
+ except KeyboardInterrupt:
411
+ pass
412
+ finally:
413
+ logger.info("Shutting down")
414
+ common.redis_client.delete(common.BASE_STREAM_NAME)
415
+ if settings.get("mode") == "master":
416
+ common.redis_client.delete(common.DISTRIBUTED_STREAM_NAME)
417
+ shutil.rmtree(settings.get("temp_dir", "./temp"))
onnx_infer.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import time
3
+ from concurrent.futures import ThreadPoolExecutor
4
+
5
+ import cv2
6
+ import numpy as np
7
+ import onnxruntime as ort
8
+ from logger import logger
9
+
10
+ import common
11
+
12
+
13
+ class OnnxSRInfer:
14
+ def __init__(
15
+ self,
16
+ model_path: str,
17
+ scale: int,
18
+ name: str,
19
+ alpha_upsampler="sr model",
20
+ providers=["CUDAExecutionProvider"],
21
+ provider_options=None,
22
+ ):
23
+ """Onnx SR Infer
24
+
25
+ Args:
26
+ model_path (str): Model path
27
+ scale (int): Model scale
28
+ name (str): Instance name,used to determine whether to continue reusing this instance or destroy it when switching models.
29
+ alpha_upsampler (str, optional): Method of SR the Alpha channel. Defaults to 'sr model'.Optionally "sr model" or "interpolation".
30
+ providers (list, optional): Ort providers. Defaults to ['DmlExecutionProvider'].
31
+ provider_options (list, optional): eg. [{'device_id': 0}]
32
+ """
33
+ self.sess = ort.InferenceSession(
34
+ model_path, providers=providers, provider_options=provider_options
35
+ )
36
+ self.name = name
37
+ self.scale = scale
38
+ self.alpha_upsampler = alpha_upsampler
39
+ self.model_path = model_path
40
+
41
+ def img_array_norm_expd(self, img):
42
+ img = np.array(img).astype(np.float32) / 255.0
43
+ img = np.transpose(img, (2, 0, 1))
44
+ img = np.expand_dims(img, axis=0)
45
+ return img
46
+
47
+ def img_array_denorm_squeeze(self, img):
48
+ output_image = np.squeeze(img)
49
+ output_image = np.transpose(output_image, (1, 2, 0))
50
+ output_image = (output_image * 255.0).clip(0, 255).astype(np.uint8)
51
+ output_image = cv2.cvtColor(output_image, cv2.COLOR_RGB2BGR)
52
+ return output_image
53
+
54
+ def mod_pad(self, img, mod=16):
55
+ """
56
+ Pad image with reflect padding along the height and width axes, based on the modulus value.
57
+
58
+ Args:
59
+ img (np.array): The input image.
60
+ mod (int): The modulus value to be used for padding. Default is 16.
61
+
62
+ Returns:
63
+ padded_img (np.array): The padded image.
64
+ pad_height (int): The added padding height.
65
+ pad_width (int): The added padding width.
66
+ """
67
+ mod_pad_h, mod_pad_w = 0, 0
68
+ h, w, _ = img.shape
69
+ if h % mod != 0:
70
+ mod_pad_h = mod - h % mod
71
+ if w % mod != 0:
72
+ mod_pad_w = mod - w % mod
73
+ pad_img = np.pad(img, ((0, mod_pad_h), (0, mod_pad_w), (0, 0)), "reflect")
74
+ return pad_img, mod_pad_h, mod_pad_w
75
+
76
+ def remove_mod_pad(self, img, pad_height, pad_width):
77
+ h, w, _ = img.shape
78
+ return img[0 : h - self.scale * pad_height, 0 : w - self.scale * pad_width, :]
79
+
80
+ def infer(self, img):
81
+ """
82
+ infer image
83
+ Args:
84
+ img (np.array)(h,w,c)
85
+ return: img (np.array)(h,w,c)
86
+ """
87
+ img = self.img_array_norm_expd(img)
88
+ img_sr = self.sess.run(["output"], {"input": img})[0]
89
+ output = self.img_array_denorm_squeeze(img_sr)
90
+ return output
91
+
92
+ def process_tile(self, img, x, y, tile_size, tile_pad, width, height, output):
93
+ """
94
+ Process a single tile and update the output image.
95
+ """
96
+ time_start = time.time()
97
+ ofs_x = x * tile_size
98
+ ofs_y = y * tile_size
99
+
100
+ # Input tile area on total image
101
+ input_start_x = ofs_x
102
+ input_end_x = min(ofs_x + tile_size, width)
103
+ input_start_y = ofs_y
104
+ input_end_y = min(ofs_y + tile_size, height)
105
+
106
+ # Input tile area on total image with padding
107
+ input_start_x_pad = max(input_start_x - tile_pad, 0)
108
+ input_end_x_pad = min(input_end_x + tile_pad, width)
109
+ input_start_y_pad = max(input_start_y - tile_pad, 0)
110
+ input_end_y_pad = min(input_end_y + tile_pad, height)
111
+
112
+ # Input tile dimensions
113
+ input_tile_width = input_end_x - input_start_x
114
+ input_tile_height = input_end_y - input_start_y
115
+
116
+ # Extract the input tile with padding
117
+ input_tile = img[
118
+ input_start_y_pad:input_end_y_pad, input_start_x_pad:input_end_x_pad, :
119
+ ]
120
+
121
+ # Infer the output tile
122
+ output_tile = self.infer(input_tile)
123
+
124
+ # Output tile area on total image
125
+ output_start_x = input_start_x * self.scale
126
+ output_end_x = input_end_x * self.scale
127
+ output_start_y = input_start_y * self.scale
128
+ output_end_y = input_end_y * self.scale
129
+
130
+ # Output tile area without padding
131
+ output_start_x_tile = (input_start_x - input_start_x_pad) * self.scale
132
+ output_end_x_tile = output_start_x_tile + input_tile_width * self.scale
133
+ output_start_y_tile = (input_start_y - input_start_y_pad) * self.scale
134
+ output_end_y_tile = output_start_y_tile + input_tile_height * self.scale
135
+
136
+ # Place the processed tile into the output image
137
+ output[output_start_y:output_end_y, output_start_x:output_end_x, :] = (
138
+ output_tile[
139
+ output_start_y_tile:output_end_y_tile,
140
+ output_start_x_tile:output_end_x_tile,
141
+ :,
142
+ ]
143
+ )
144
+ logger.debug(
145
+ f"Processed tile {x},{y} in {time.time() - time_start:.2f} seconds"
146
+ )
147
+
148
+ def tile_process(
149
+ self,
150
+ img,
151
+ tile_size,
152
+ tile_pad=8,
153
+ max_workers=common.MAX_THREAD,
154
+ ):
155
+ """
156
+ It will first crop input images to tiles, and then process each tile.
157
+ Finally, all the processed tiles are merged into one images.
158
+ Args:
159
+ img (np.array)(h,w,c): image to be processed.
160
+ tile_size (int): tile size.
161
+ tile_pad (int):tile pad size.
162
+ return: img (np.array)(h,w,c): processed image.
163
+ Modified from: https://github.com/ata4/esrgan-launcher
164
+ """
165
+ height, width, channels = img.shape
166
+ logger.debug(f"input_shape: {img.shape}")
167
+ output_height = height * self.scale
168
+ output_width = width * self.scale
169
+ output_shape = (output_height, output_width, channels)
170
+ logger.debug(f"output_shape: {output_shape}")
171
+
172
+ # start with black image
173
+ logger.debug(f"tail size: {tile_size}")
174
+ output = np.zeros(output_shape, dtype=np.float32)
175
+ tiles_x = math.ceil(width / tile_size)
176
+ tiles_y = math.ceil(height / tile_size)
177
+ logger.debug(
178
+ f"tiles_x: {tiles_x}, tiles_y: {tiles_y}, total tiles: {tiles_x * tiles_y}"
179
+ )
180
+
181
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
182
+ futures = []
183
+ for y in range(tiles_y):
184
+ for x in range(tiles_x):
185
+ futures.append(
186
+ executor.submit(
187
+ self.process_tile,
188
+ img,
189
+ x,
190
+ y,
191
+ tile_size,
192
+ tile_pad,
193
+ width,
194
+ height,
195
+ output,
196
+ )
197
+ )
198
+
199
+ for future in futures:
200
+ future.result()
201
+
202
+ return output
203
+
204
+ def rgb_process_pipeline(self, image, tile_size):
205
+ # mod pad
206
+ pad_img, pad_h, pad_w = self.mod_pad(image)
207
+ # tile process
208
+ sr_img = self.tile_process(pad_img, tile_size)
209
+ # remove pad
210
+ final_img = self.remove_mod_pad(sr_img, pad_h, pad_w)
211
+ return final_img
212
+
213
+ def universal_process_pipeline(self, image, tile_size):
214
+ logger.info(f"Processing image with {self.name}...")
215
+ img_mode = "RGB"
216
+ h, w, c = image.shape
217
+ # handle RGBA image
218
+ if c == 4:
219
+ img_mode = "RGBA"
220
+ alpha = image[:, :, 3]
221
+ image = image[:, :, 0:3]
222
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
223
+ if self.alpha_upsampler == "sr model":
224
+ alpha = cv2.cvtColor(alpha, cv2.COLOR_GRAY2RGB)
225
+ else:
226
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
227
+ # process image (without alpha channel)
228
+ output_img = self.rgb_process_pipeline(image, tile_size)
229
+ # process alpha channel
230
+ if img_mode == "RGBA":
231
+ if self.alpha_upsampler == "sr model":
232
+ alpha_img = self.rgb_process_pipeline(alpha, tile_size)
233
+ output_alpha = cv2.cvtColor(alpha_img, cv2.COLOR_BGR2GRAY)
234
+ else: # use the cv2 resize for alpha channel
235
+ output_alpha = cv2.resize(
236
+ alpha,
237
+ (w * self.scale, h * self.scale),
238
+ interpolation=cv2.INTER_LINEAR,
239
+ )
240
+ # merge the alpha channel
241
+ output_img = cv2.cvtColor(output_img, cv2.COLOR_BGR2BGRA)
242
+ output_img[:, :, 3] = output_alpha
243
+ return output_img
pyproject.toml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "super-resolution-api"
3
+ version = "0.1.0"
4
+ description = "REST API for super resolution"
5
+ readme = "README.md"
6
+ license = { file = "LICENSE" }
7
+ requires-python = ">=3.11"
8
+ dependencies = [
9
+ "dynaconf>=3.2.6",
10
+ "fastapi>=0.115.6",
11
+ "func-timeout>=4.3.5",
12
+ "httpx>=0.28.1",
13
+ "loguru>=0.7.3",
14
+ "numpy>=2.1.3",
15
+ "onnxruntime>=1.20.1",
16
+ "opencv-python>=4.10.0.84",
17
+ "python-multipart>=0.0.19",
18
+ "redis>=5.2.1",
19
+ "uvicorn>=0.32.1",
20
+ "uvloop>=0.21.0",
21
+ ]
22
+
23
+ [dependency-groups]
24
+ dev = ["ruff>=0.8.2"]
requirements.txt ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv export --no-dev --no-hashes -o requirements.txt
3
+ annotated-types==0.7.0
4
+ anyio==4.7.0
5
+ async-timeout==5.0.1 ; python_full_version < '3.11.3'
6
+ certifi==2024.8.30
7
+ click==8.1.7
8
+ colorama==0.4.6 ; sys_platform == 'win32' or platform_system == 'Windows'
9
+ coloredlogs==15.0.1
10
+ dynaconf==3.2.6
11
+ fastapi==0.115.6
12
+ flatbuffers==24.3.25
13
+ func-timeout==4.3.5
14
+ h11==0.14.0
15
+ httpcore==1.0.7
16
+ httpx==0.28.1
17
+ humanfriendly==10.0
18
+ idna==3.10
19
+ loguru==0.7.3
20
+ mpmath==1.3.0
21
+ numpy==2.1.3
22
+ onnxruntime==1.20.1
23
+ opencv-python==4.10.0.84
24
+ packaging==24.2
25
+ protobuf==5.29.1
26
+ pydantic==2.10.3
27
+ pydantic-core==2.27.1
28
+ pyreadline3==3.5.4 ; sys_platform == 'win32'
29
+ python-multipart==0.0.19
30
+ redis==5.2.1
31
+ sniffio==1.3.1
32
+ starlette==0.41.3
33
+ sympy==1.13.3
34
+ typing-extensions==4.12.2
35
+ uvicorn==0.32.1
36
+ uvloop==0.21.0
37
+ websockets==14.1
38
+ win32-setctime==1.1.0 ; sys_platform == 'win32'
settings.toml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ provider = "CPUExecutionProvider"
2
+ token = "qwqowo"
3
+ redis_url = "redis://localhost:6379"
4
+ host = "0.0.0.0"
5
+ port = 39721
6
+ max_timeout = 300
7
+ timeout = 30
8
+ max_thread = 8
9
+ mode = "master" # master, slave, single
10
+ master_url = "http://localhost:39721"
11
+ master_token = "qwqowo"
12
+ worker_id = ""
13
+ worker_url = "http://localhost:39721"
14
+ temp_dir = "./temp"
15
+ output_dir = "./output"
16
+ log_level = "DEBUG"
17
+ worker_check_interval = 5
18
+ worker_expire = 120
19
+ register_interval = 30
sr_queue.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import math
3
+ import pickle
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import cv2
8
+ import httpx
9
+ import numpy as np
10
+ from func_timeout import func_set_timeout
11
+ from func_timeout.exceptions import FunctionTimedOut
12
+ from logger import logger
13
+
14
+ import common
15
+ from config import settings
16
+ from onnx_infer import OnnxSRInfer
17
+
18
+
19
+ @func_set_timeout(common.PROGRESS_TIMEOUT, allowOverride=True)
20
+ def _process_image(
21
+ model: common.ModelInfo = common.models[common.MODEL_NAME_DEFAULT],
22
+ tile_size: int = 64, # 分块大小
23
+ scale: int = 4, # 放大倍数
24
+ skip_alpha: bool = False, # 是否跳过alpha通道
25
+ resize_to: str = None, # 调整大小 两种格式: 1. 1920x1080 2. 1/2
26
+ input_image: Path = None,
27
+ output_path: Path | str = settings.get("output_dir", "output"),
28
+ gpuid: int = 0,
29
+ clean: bool = True,
30
+ ) -> Path:
31
+ logger.info(f"processing image: {input_image}")
32
+ start_time = datetime.datetime.now()
33
+ try:
34
+ provider_options = None
35
+ if int(gpuid) >= 0:
36
+ provider_options = [{"device_id": int(gpuid)}]
37
+ sr_instance = OnnxSRInfer(
38
+ model.path,
39
+ model.scale,
40
+ model.name,
41
+ providers=[settings.get("provider", "CPUExecutionProvider")],
42
+ provider_options=provider_options,
43
+ )
44
+ if skip_alpha:
45
+ logger.debug("Skip Alpha Channel")
46
+ sr_instance.alpha_upsampler = "interpolation"
47
+ logger.debug(f"decoding image: {input_image}")
48
+ img = cv2.imdecode(
49
+ np.fromfile(input_image, dtype=np.uint8), cv2.IMREAD_UNCHANGED
50
+ )
51
+ h, w, _ = img.shape
52
+ sr_img = sr_instance.universal_process_pipeline(img, tile_size=tile_size)
53
+ scale = int(scale)
54
+ target_h = None
55
+ target_w = None
56
+ if scale > model.scale and model.scale != 1:
57
+ logger.debug("re process")
58
+ # calc process times
59
+ scale_log = math.log(scale, model.scale)
60
+ total_times = math.ceil(scale_log)
61
+ # calc target size
62
+ if total_times != int(scale_log):
63
+ target_h = h * scale
64
+ target_w = w * scale
65
+
66
+ for _ in range(total_times - 1):
67
+ sr_img = sr_instance.universal_process_pipeline(
68
+ sr_img, tile_size=tile_size
69
+ )
70
+ elif scale < model.scale:
71
+ logger.debug("down scale")
72
+ target_h = h * scale
73
+ target_w = w * scale
74
+
75
+ if resize_to:
76
+ logger.debug(f"resize to {resize_to}")
77
+ if "x" in resize_to:
78
+ param_w = int(resize_to.split("x")[0])
79
+ target_w = param_w
80
+ target_h = int(h * param_w / w)
81
+ elif "/" in resize_to:
82
+ ratio = int(resize_to.split("/")[0]) / int(resize_to.split("/")[1])
83
+ target_w = int(w * ratio)
84
+ target_h = int(h * ratio)
85
+
86
+ if target_w:
87
+ logger.debug(f"resize to {target_w}x{target_h}")
88
+ img_out = cv2.resize(sr_img, (target_w, target_h))
89
+ else:
90
+ img_out = sr_img
91
+ # save
92
+ final_output_path = Path(output_path) / f"{input_image.stem}_{model.name}.png"
93
+ if not Path(output_path).exists():
94
+ Path(output_path).mkdir(parents=True)
95
+ cv2.imencode(".png", img_out)[1].tofile(final_output_path)
96
+ return final_output_path
97
+ except Exception as e:
98
+ logger.error(f"process image error: {e}")
99
+ return None
100
+ finally:
101
+ logger.info(
102
+ f"Time taken: {(datetime.datetime.now() - start_time).seconds} seconds to process {input_image}"
103
+ )
104
+ if clean and input_image.exists():
105
+ input_image.unlink()
106
+
107
+
108
+ def listen_queue(
109
+ stream_name: str = common.BASE_STREAM_NAME,
110
+ default_timeout: int = common.PROGRESS_TIMEOUT,
111
+ ):
112
+ logger.info(f"Listening to stream: {stream_name}")
113
+ last_id = "0"
114
+ while True:
115
+ messages = common.redis_client.xread({stream_name: last_id}, count=1, block=0)
116
+ if not messages:
117
+ continue
118
+ message_id = messages[0][1][0][0]
119
+ last_id = message_id
120
+ message = messages[0][1][0][1]
121
+ logger.info(f"Processing task: {message_id.decode('utf-8')}")
122
+ data: dict[str, Path | int | bool | str | None] = pickle.loads(message[b"data"])
123
+ input_image = data.get("input_image")
124
+ tile_size = data.get("tile_size", 64)
125
+ scale = data.get("scale", 4)
126
+ skip_alpha = data.get("skip_alpha", False)
127
+ resize_to = data.get("resize_to", None)
128
+ time_out = data.get("timeout", default_timeout)
129
+ model_name = data.get("model", common.MODEL_NAME_DEFAULT)
130
+ common.redis_client.set(
131
+ f"{common.RESULT_KEY_PREFIX}{message_id.decode('utf-8')}",
132
+ pickle.dumps({"status": "processing"}),
133
+ ex=86400,
134
+ )
135
+ processed_path: Path | None = None
136
+ try:
137
+ processed_path = _process_image(
138
+ model=common.models[model_name],
139
+ input_image=input_image,
140
+ tile_size=tile_size,
141
+ scale=scale,
142
+ skip_alpha=skip_alpha,
143
+ resize_to=resize_to,
144
+ forceTimeout=time_out,
145
+ )
146
+ except FunctionTimedOut as e:
147
+ logger.warning(e)
148
+ processed_path = None
149
+ if processed_path:
150
+ common.redis_client.set(
151
+ f"{common.RESULT_KEY_PREFIX}{message_id.decode('utf-8')}",
152
+ pickle.dumps(
153
+ {
154
+ "status": "success",
155
+ "path": processed_path.as_posix(),
156
+ "size": processed_path.stat().st_size,
157
+ }
158
+ ),
159
+ ex=86400,
160
+ )
161
+ logger.success(f"Processed image: {processed_path}")
162
+ else:
163
+ common.redis_client.set(
164
+ f"{common.RESULT_KEY_PREFIX}{message_id.decode('utf-8')}",
165
+ pickle.dumps({"status": "failed"}),
166
+ ex=86400,
167
+ )
168
+ common.redis_client.xdel(stream_name, message_id)
169
+ for file in Path(settings.get("output_dir", "output")).iterdir():
170
+ if datetime.datetime.now().timestamp() - file.stat().st_mtime > 86400:
171
+ file.unlink()
172
+
173
+
174
+ def listen_distributed_queue(stream_name: str = common.DISTRIBUTED_STREAM_NAME):
175
+ logger.info(f"Listening to distributed stream: {stream_name}")
176
+ last_id = "0"
177
+ while True:
178
+ messages = common.redis_client.xread({stream_name: last_id}, count=1, block=0)
179
+ if not messages:
180
+ continue
181
+ task_id = messages[0][1][0][0]
182
+ last_id = task_id
183
+ message = messages[0][1][0][1]
184
+ logger.info(f"Processing task: {task_id.decode('utf-8')}")
185
+ time_start = datetime.datetime.now()
186
+ data: dict = pickle.loads(message[b"data"])
187
+ worker_response: dict = data.get("worker_response")
188
+ input_image = data.get("input_image")
189
+ input_image: Path
190
+ scale: int = data.get("scale", 4)
191
+
192
+ common.redis_client.set(
193
+ f"{common.RESULT_KEY_PREFIX}{task_id.decode('utf-8')}",
194
+ pickle.dumps({"status": "processing"}),
195
+ ex=86400,
196
+ )
197
+ original_w, original_h = common.get_image_size(input_image)
198
+ ok_keys = []
199
+ scaled_tiles: list[common.TileInfo] = []
200
+ while True:
201
+ try:
202
+ for worker_key, worker_data in worker_response.items():
203
+ logger.debug(f"Checking worker: {worker_key.decode('utf-8')}")
204
+ worker = common.redis_client.get(worker_key)
205
+ if not worker:
206
+ raise Exception(f"Worker {worker_key.decode('utf-8')} offline")
207
+ worker_url, token = worker.decode("utf-8").split("|")
208
+ worker_task_id = worker_data["task_id"]
209
+ response = httpx.get(
210
+ f"{worker_url}/result/{worker_task_id}",
211
+ headers={"X-Token": token},
212
+ )
213
+ if response.status_code != 200:
214
+ raise Exception(
215
+ f"Worker {worker_key.decode('utf-8')} get task status failed"
216
+ )
217
+ result = response.json()["result"]
218
+ if result["status"] == "failed":
219
+ raise Exception(
220
+ f"Worker {worker_key.decode('utf-8')} processing failed"
221
+ )
222
+ if result["status"] == "success":
223
+ logger.info(f"Worker {worker_key.decode('utf-8')} processed")
224
+ response = httpx.get(
225
+ f"{worker_url}/result/{worker_task_id}/download",
226
+ headers={"X-Token": token},
227
+ )
228
+ if response.status_code != 200:
229
+ raise Exception(
230
+ f"Worker {worker_key.decode('utf-8')} download failed"
231
+ )
232
+ tile_info: common.TileInfo = worker_data["tile_info"]
233
+ file_path = (
234
+ Path(settings.get("output_dir", "output"))
235
+ / f"{input_image.stem}"
236
+ / f"{input_image.stem}_scaled_{tile_info.y}_{tile_info.x}.png"
237
+ )
238
+ with open(file_path, "wb") as f:
239
+ f.write(response.content)
240
+ logger.debug(f"Downloaded tile: {file_path}")
241
+ scaled_tiles.append(
242
+ common.TileInfo(tile_info.x, tile_info.y, file_path)
243
+ )
244
+ ok_keys.append(worker_key)
245
+
246
+ for key in ok_keys:
247
+ worker_response.pop(key, None)
248
+
249
+ if not worker_response:
250
+ logger.info(
251
+ f"All workers processed, start merge {len(scaled_tiles)} tiles"
252
+ )
253
+ output_path = (
254
+ Path(settings.get("output_dir", "output"))
255
+ / f"{input_image.stem}"
256
+ / f"{input_image.stem}_scaled_x{scale}.png"
257
+ )
258
+ common.merge_sr_tiles(
259
+ scaled_tiles,
260
+ output_path,
261
+ (original_w, original_h),
262
+ scale,
263
+ )
264
+ logger.success(
265
+ f"Processed image: {output_path}, time taken: {(datetime.datetime.now() - time_start).seconds} seconds"
266
+ )
267
+ common.redis_client.set(
268
+ f"{common.RESULT_KEY_PREFIX}{task_id.decode('utf-8')}",
269
+ pickle.dumps(
270
+ {
271
+ "status": "success",
272
+ "path": output_path.as_posix(),
273
+ "size": output_path.stat().st_size,
274
+ }
275
+ ),
276
+ ex=86400,
277
+ )
278
+ break
279
+
280
+ time.sleep(settings.get("worker_check_interval", 5))
281
+ except Exception as e:
282
+ logger.error(f"{e.__class__.__name__}: {e}")
283
+ common.redis_client.set(
284
+ f"{common.RESULT_KEY_PREFIX}{task_id.decode('utf-8')}",
285
+ pickle.dumps({"status": "failed"}),
286
+ ex=86400,
287
+ )
288
+ break
test_common.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+
3
+
4
+ class TestCalculateTiles(unittest.TestCase):
5
+ def setUp(self):
6
+ """初始化测试数据"""
7
+ self.image_width = 1920
8
+ self.image_height = 1080
9
+
10
+ def test_exact_match_workers(self):
11
+ """测试 Worker 数量与网格数完全匹配"""
12
+ from common import calculate_grid
13
+
14
+ rows, cols = calculate_grid(self.image_width, self.image_height, 3)
15
+ self.assertEqual(rows * cols, 3)
16
+ self.assertEqual((rows, cols), (1, 3)) # 宽大于高时优先横向切割
17
+
18
+ def test_square_preference(self):
19
+ """测试尽量生成接近正方形的网格"""
20
+ from common import calculate_grid
21
+
22
+ rows, cols = calculate_grid(self.image_width, self.image_height, 4)
23
+ self.assertEqual(rows * cols, 4)
24
+ self.assertEqual((rows, cols), (2, 2)) # 4块时优先正方形分割
25
+
26
+ def test_large_workers(self):
27
+ """测试较多 Worker 数量"""
28
+ from common import calculate_grid
29
+
30
+ rows, cols = calculate_grid(self.image_width, self.image_height, 12)
31
+ self.assertEqual(rows * cols, 12)
32
+ self.assertEqual((rows, cols), (3, 4)) # 优化为接近宽高比的分割
33
+
34
+ def test_one_worker(self):
35
+ """测试单个 Worker 的情况"""
36
+ from common import calculate_grid
37
+
38
+ rows, cols = calculate_grid(self.image_width, self.image_height, 1)
39
+ self.assertEqual(rows * cols, 1)
40
+ self.assertEqual((rows, cols), (1, 1))
41
+
42
+ def test_invalid_worker_count(self):
43
+ """测试 Worker 数量为 0 或负数的情况"""
44
+ from common import calculate_grid
45
+
46
+ with self.assertRaises(ValueError):
47
+ calculate_grid(self.image_width, self.image_height, 0)
48
+
49
+ with self.assertRaises(ValueError):
50
+ calculate_grid(self.image_width, self.image_height, -1)
51
+
52
+ def test_aspect_ratio_preservation(self):
53
+ """测试长宽比优先调整"""
54
+ from common import calculate_grid
55
+
56
+ rows, cols = calculate_grid(1080, 1920, 4) # 竖向图片
57
+ self.assertEqual(rows * cols, 4)
58
+ self.assertEqual((rows, cols), (2, 2)) # 竖向也应保持正方形优先
59
+
60
+ def test_non_divisible_workers(self):
61
+ """测试当 workers 不能被均匀分割时"""
62
+ from common import calculate_grid
63
+
64
+ rows, cols = calculate_grid(self.image_width, self.image_height, 5)
65
+ self.assertEqual(rows * cols, 5)
66
+ # 检查返回的行列数是否正确
67
+ self.assertTrue(rows == 1 and cols == 5 or rows == 5 and cols == 1)
68
+
69
+ def test_large_image_size(self):
70
+ """测试非常大的图像尺寸"""
71
+ from common import calculate_grid
72
+
73
+ rows, cols = calculate_grid(8000, 8000, 16)
74
+ self.assertEqual(rows * cols, 16)
75
+ self.assertEqual((rows, cols), (4, 4)) # 优先正方形分割
76
+
77
+ def test_wide_image(self):
78
+ """测试宽幅图像"""
79
+ from common import calculate_grid
80
+
81
+ rows, cols = calculate_grid(4000, 1000, 8)
82
+ self.assertEqual(rows * cols, 8)
83
+ # 检查是否更倾向于横向分割
84
+ self.assertTrue(cols > rows)
85
+
86
+ def test_tall_image(self):
87
+ """测试高幅图像"""
88
+ from common import calculate_grid
89
+
90
+ rows, cols = calculate_grid(1000, 4000, 8)
91
+ self.assertEqual(rows * cols, 8)
92
+ # 检查是否更倾向于纵向分割
93
+ self.assertTrue(rows > cols)
94
+
95
+ def test_prime_number_workers(self):
96
+ """测试工作者数量为质数的情况"""
97
+ from common import calculate_grid
98
+
99
+ rows, cols = calculate_grid(self.image_width, self.image_height, 7)
100
+ self.assertEqual(rows * cols, 7)
101
+ # 检查返回的行列数是否正确
102
+ self.assertTrue(rows == 1 and cols == 7 or rows == 7 and cols == 1)
103
+
104
+ def test_zero_image_size(self):
105
+ """测试图像尺寸为 0 的情况"""
106
+ from common import calculate_grid
107
+
108
+ with self.assertRaises(ZeroDivisionError):
109
+ calculate_grid(0, self.image_height, 4)
110
+ with self.assertRaises(ZeroDivisionError):
111
+ calculate_grid(self.image_width, 0, 4)
112
+
113
+
114
+ if __name__ == "__main__":
115
+ unittest.main()
uv.lock ADDED
@@ -0,0 +1,576 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+ requires-python = ">=3.11"
3
+ resolution-markers = [
4
+ "python_full_version < '3.12' and platform_system == 'Darwin'",
5
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and platform_system == 'Linux'",
6
+ "(python_full_version < '3.12' and platform_machine != 'aarch64' and platform_system != 'Darwin') or (python_full_version < '3.12' and platform_system != 'Darwin' and platform_system != 'Linux')",
7
+ "python_full_version >= '3.12' and platform_system == 'Darwin'",
8
+ "python_full_version >= '3.12' and platform_machine == 'aarch64' and platform_system == 'Linux'",
9
+ "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_system != 'Darwin') or (python_full_version >= '3.12' and platform_system != 'Darwin' and platform_system != 'Linux')",
10
+ ]
11
+
12
+ [[package]]
13
+ name = "annotated-types"
14
+ version = "0.7.0"
15
+ source = { registry = "https://pypi.org/simple" }
16
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
17
+ wheels = [
18
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
19
+ ]
20
+
21
+ [[package]]
22
+ name = "anyio"
23
+ version = "4.7.0"
24
+ source = { registry = "https://pypi.org/simple" }
25
+ dependencies = [
26
+ { name = "idna" },
27
+ { name = "sniffio" },
28
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
29
+ ]
30
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/40/318e58f669b1a9e00f5c4453910682e2d9dd594334539c7b7817dabb765f/anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48", size = 177076 }
31
+ wheels = [
32
+ { url = "https://files.pythonhosted.org/packages/a0/7a/4daaf3b6c08ad7ceffea4634ec206faeff697526421c20f07628c7372156/anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352", size = 93052 },
33
+ ]
34
+
35
+ [[package]]
36
+ name = "async-timeout"
37
+ version = "5.0.1"
38
+ source = { registry = "https://pypi.org/simple" }
39
+ sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 }
40
+ wheels = [
41
+ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 },
42
+ ]
43
+
44
+ [[package]]
45
+ name = "certifi"
46
+ version = "2024.8.30"
47
+ source = { registry = "https://pypi.org/simple" }
48
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
49
+ wheels = [
50
+ { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
51
+ ]
52
+
53
+ [[package]]
54
+ name = "click"
55
+ version = "8.1.7"
56
+ source = { registry = "https://pypi.org/simple" }
57
+ dependencies = [
58
+ { name = "colorama", marker = "platform_system == 'Windows'" },
59
+ ]
60
+ sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
61
+ wheels = [
62
+ { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
63
+ ]
64
+
65
+ [[package]]
66
+ name = "colorama"
67
+ version = "0.4.6"
68
+ source = { registry = "https://pypi.org/simple" }
69
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
70
+ wheels = [
71
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
72
+ ]
73
+
74
+ [[package]]
75
+ name = "coloredlogs"
76
+ version = "15.0.1"
77
+ source = { registry = "https://pypi.org/simple" }
78
+ dependencies = [
79
+ { name = "humanfriendly" },
80
+ ]
81
+ sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520 }
82
+ wheels = [
83
+ { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 },
84
+ ]
85
+
86
+ [[package]]
87
+ name = "dynaconf"
88
+ version = "3.2.6"
89
+ source = { registry = "https://pypi.org/simple" }
90
+ sdist = { url = "https://files.pythonhosted.org/packages/56/1a/324f1bf234cc4f98445305fd8719245318466e310e05caea7ef052748ecd/dynaconf-3.2.6.tar.gz", hash = "sha256:74cc1897396380bb957730eb341cc0976ee9c38bbcb53d3307c50caed0aedfb8", size = 229209 }
91
+ wheels = [
92
+ { url = "https://files.pythonhosted.org/packages/e2/14/c8a7d861262139688fa465d2e27ff7113764d6fa03b15b9c7b666729ea2e/dynaconf-3.2.6-py2.py3-none-any.whl", hash = "sha256:3911c740d717df4576ed55f616c7cbad6e06bc8ef23ffca444b6e2a12fb1c34c", size = 231063 },
93
+ ]
94
+
95
+ [[package]]
96
+ name = "fastapi"
97
+ version = "0.115.6"
98
+ source = { registry = "https://pypi.org/simple" }
99
+ dependencies = [
100
+ { name = "pydantic" },
101
+ { name = "starlette" },
102
+ { name = "typing-extensions" },
103
+ ]
104
+ sdist = { url = "https://files.pythonhosted.org/packages/93/72/d83b98cd106541e8f5e5bfab8ef2974ab45a62e8a6c5b5e6940f26d2ed4b/fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654", size = 301336 }
105
+ wheels = [
106
+ { url = "https://files.pythonhosted.org/packages/52/b3/7e4df40e585df024fac2f80d1a2d579c854ac37109675db2b0cc22c0bb9e/fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305", size = 94843 },
107
+ ]
108
+
109
+ [[package]]
110
+ name = "flatbuffers"
111
+ version = "24.3.25"
112
+ source = { registry = "https://pypi.org/simple" }
113
+ sdist = { url = "https://files.pythonhosted.org/packages/a9/74/2df95ef84b214d2bee0886d572775a6f38793f5ca6d7630c3239c91104ac/flatbuffers-24.3.25.tar.gz", hash = "sha256:de2ec5b203f21441716617f38443e0a8ebf3d25bf0d9c0bb0ce68fa00ad546a4", size = 22139 }
114
+ wheels = [
115
+ { url = "https://files.pythonhosted.org/packages/41/f0/7e988a019bc54b2dbd0ad4182ef2d53488bb02e58694cd79d61369e85900/flatbuffers-24.3.25-py2.py3-none-any.whl", hash = "sha256:8dbdec58f935f3765e4f7f3cf635ac3a77f83568138d6a2311f524ec96364812", size = 26784 },
116
+ ]
117
+
118
+ [[package]]
119
+ name = "func-timeout"
120
+ version = "4.3.5"
121
+ source = { registry = "https://pypi.org/simple" }
122
+ sdist = { url = "https://files.pythonhosted.org/packages/b3/0d/bf0567477f7281d9a3926c582bfef21bff7498fc0ffd3e9de21811896a0b/func_timeout-4.3.5.tar.gz", hash = "sha256:74cd3c428ec94f4edfba81f9b2f14904846d5ffccc27c92433b8b5939b5575dd", size = 44264 }
123
+
124
+ [[package]]
125
+ name = "h11"
126
+ version = "0.14.0"
127
+ source = { registry = "https://pypi.org/simple" }
128
+ sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
129
+ wheels = [
130
+ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
131
+ ]
132
+
133
+ [[package]]
134
+ name = "httpcore"
135
+ version = "1.0.7"
136
+ source = { registry = "https://pypi.org/simple" }
137
+ dependencies = [
138
+ { name = "certifi" },
139
+ { name = "h11" },
140
+ ]
141
+ sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
142
+ wheels = [
143
+ { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
144
+ ]
145
+
146
+ [[package]]
147
+ name = "httpx"
148
+ version = "0.28.1"
149
+ source = { registry = "https://pypi.org/simple" }
150
+ dependencies = [
151
+ { name = "anyio" },
152
+ { name = "certifi" },
153
+ { name = "httpcore" },
154
+ { name = "idna" },
155
+ ]
156
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
157
+ wheels = [
158
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
159
+ ]
160
+
161
+ [[package]]
162
+ name = "humanfriendly"
163
+ version = "10.0"
164
+ source = { registry = "https://pypi.org/simple" }
165
+ dependencies = [
166
+ { name = "pyreadline3", marker = "sys_platform == 'win32'" },
167
+ ]
168
+ sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 }
169
+ wheels = [
170
+ { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 },
171
+ ]
172
+
173
+ [[package]]
174
+ name = "idna"
175
+ version = "3.10"
176
+ source = { registry = "https://pypi.org/simple" }
177
+ sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
178
+ wheels = [
179
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
180
+ ]
181
+
182
+ [[package]]
183
+ name = "loguru"
184
+ version = "0.7.3"
185
+ source = { registry = "https://pypi.org/simple" }
186
+ dependencies = [
187
+ { name = "colorama", marker = "sys_platform == 'win32'" },
188
+ { name = "win32-setctime", marker = "sys_platform == 'win32'" },
189
+ ]
190
+ sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 }
191
+ wheels = [
192
+ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 },
193
+ ]
194
+
195
+ [[package]]
196
+ name = "mpmath"
197
+ version = "1.3.0"
198
+ source = { registry = "https://pypi.org/simple" }
199
+ sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 }
200
+ wheels = [
201
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 },
202
+ ]
203
+
204
+ [[package]]
205
+ name = "numpy"
206
+ version = "2.1.3"
207
+ source = { registry = "https://pypi.org/simple" }
208
+ sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090 }
209
+ wheels = [
210
+ { url = "https://files.pythonhosted.org/packages/ad/81/c8167192eba5247593cd9d305ac236847c2912ff39e11402e72ae28a4985/numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", size = 21156252 },
211
+ { url = "https://files.pythonhosted.org/packages/da/74/5a60003fc3d8a718d830b08b654d0eea2d2db0806bab8f3c2aca7e18e010/numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", size = 13784119 },
212
+ { url = "https://files.pythonhosted.org/packages/47/7c/864cb966b96fce5e63fcf25e1e4d957fe5725a635e5f11fe03f39dd9d6b5/numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", size = 5352978 },
213
+ { url = "https://files.pythonhosted.org/packages/09/ac/61d07930a4993dd9691a6432de16d93bbe6aa4b1c12a5e573d468eefc1ca/numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09", size = 6892570 },
214
+ { url = "https://files.pythonhosted.org/packages/27/2f/21b94664f23af2bb52030653697c685022119e0dc93d6097c3cb45bce5f9/numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", size = 13896715 },
215
+ { url = "https://files.pythonhosted.org/packages/7a/f0/80811e836484262b236c684a75dfc4ba0424bc670e765afaa911468d9f39/numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", size = 16339644 },
216
+ { url = "https://files.pythonhosted.org/packages/fa/81/ce213159a1ed8eb7d88a2a6ef4fbdb9e4ffd0c76b866c350eb4e3c37e640/numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", size = 16712217 },
217
+ { url = "https://files.pythonhosted.org/packages/7d/84/4de0b87d5a72f45556b2a8ee9fc8801e8518ec867fc68260c1f5dcb3903f/numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", size = 14399053 },
218
+ { url = "https://files.pythonhosted.org/packages/7e/1c/e5fabb9ad849f9d798b44458fd12a318d27592d4bc1448e269dec070ff04/numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", size = 6534741 },
219
+ { url = "https://files.pythonhosted.org/packages/1e/48/a9a4b538e28f854bfb62e1dea3c8fea12e90216a276c7777ae5345ff29a7/numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", size = 12869487 },
220
+ { url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658 },
221
+ { url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258 },
222
+ { url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249 },
223
+ { url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704 },
224
+ { url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089 },
225
+ { url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185 },
226
+ { url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751 },
227
+ { url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705 },
228
+ { url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077 },
229
+ { url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858 },
230
+ { url = "https://files.pythonhosted.org/packages/4d/0b/620591441457e25f3404c8057eb924d04f161244cb8a3680d529419aa86e/numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", size = 20836263 },
231
+ { url = "https://files.pythonhosted.org/packages/45/e1/210b2d8b31ce9119145433e6ea78046e30771de3fe353f313b2778142f34/numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", size = 13507771 },
232
+ { url = "https://files.pythonhosted.org/packages/55/44/aa9ee3caee02fa5a45f2c3b95cafe59c44e4b278fbbf895a93e88b308555/numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", size = 5075805 },
233
+ { url = "https://files.pythonhosted.org/packages/78/d6/61de6e7e31915ba4d87bbe1ae859e83e6582ea14c6add07c8f7eefd8488f/numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", size = 6608380 },
234
+ { url = "https://files.pythonhosted.org/packages/3e/46/48bdf9b7241e317e6cf94276fe11ba673c06d1fdf115d8b4ebf616affd1a/numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", size = 13602451 },
235
+ { url = "https://files.pythonhosted.org/packages/70/50/73f9a5aa0810cdccda9c1d20be3cbe4a4d6ea6bfd6931464a44c95eef731/numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", size = 16039822 },
236
+ { url = "https://files.pythonhosted.org/packages/ad/cd/098bc1d5a5bc5307cfc65ee9369d0ca658ed88fbd7307b0d49fab6ca5fa5/numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", size = 16411822 },
237
+ { url = "https://files.pythonhosted.org/packages/83/a2/7d4467a2a6d984549053b37945620209e702cf96a8bc658bc04bba13c9e2/numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", size = 14079598 },
238
+ { url = "https://files.pythonhosted.org/packages/e9/6a/d64514dcecb2ee70bfdfad10c42b76cab657e7ee31944ff7a600f141d9e9/numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", size = 6236021 },
239
+ { url = "https://files.pythonhosted.org/packages/bb/f9/12297ed8d8301a401e7d8eb6b418d32547f1d700ed3c038d325a605421a4/numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", size = 12560405 },
240
+ { url = "https://files.pythonhosted.org/packages/a7/45/7f9244cd792e163b334e3a7f02dff1239d2890b6f37ebf9e82cbe17debc0/numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", size = 20859062 },
241
+ { url = "https://files.pythonhosted.org/packages/b1/b4/a084218e7e92b506d634105b13e27a3a6645312b93e1c699cc9025adb0e1/numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", size = 13515839 },
242
+ { url = "https://files.pythonhosted.org/packages/27/45/58ed3f88028dcf80e6ea580311dc3edefdd94248f5770deb980500ef85dd/numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", size = 5116031 },
243
+ { url = "https://files.pythonhosted.org/packages/37/a8/eb689432eb977d83229094b58b0f53249d2209742f7de529c49d61a124a0/numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", size = 6629977 },
244
+ { url = "https://files.pythonhosted.org/packages/42/a3/5355ad51ac73c23334c7caaed01adadfda49544f646fcbfbb4331deb267b/numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", size = 13575951 },
245
+ { url = "https://files.pythonhosted.org/packages/c4/70/ea9646d203104e647988cb7d7279f135257a6b7e3354ea6c56f8bafdb095/numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", size = 16022655 },
246
+ { url = "https://files.pythonhosted.org/packages/14/ce/7fc0612903e91ff9d0b3f2eda4e18ef9904814afcae5b0f08edb7f637883/numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", size = 16399902 },
247
+ { url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180 },
248
+ { url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907 },
249
+ { url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098 },
250
+ ]
251
+
252
+ [[package]]
253
+ name = "onnxruntime"
254
+ version = "1.20.1"
255
+ source = { registry = "https://pypi.org/simple" }
256
+ dependencies = [
257
+ { name = "coloredlogs" },
258
+ { name = "flatbuffers" },
259
+ { name = "numpy" },
260
+ { name = "packaging" },
261
+ { name = "protobuf" },
262
+ { name = "sympy" },
263
+ ]
264
+ wheels = [
265
+ { url = "https://files.pythonhosted.org/packages/95/8d/2634e2959b34aa8a0037989f4229e9abcfa484e9c228f99633b3241768a6/onnxruntime-1.20.1-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:06bfbf02ca9ab5f28946e0f912a562a5f005301d0c419283dc57b3ed7969bb7b", size = 30998725 },
266
+ { url = "https://files.pythonhosted.org/packages/a5/da/c44bf9bd66cd6d9018a921f053f28d819445c4d84b4dd4777271b0fe52a2/onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6243e34d74423bdd1edf0ae9596dd61023b260f546ee17d701723915f06a9f7", size = 11955227 },
267
+ { url = "https://files.pythonhosted.org/packages/11/ac/4120dfb74c8e45cce1c664fc7f7ce010edd587ba67ac41489f7432eb9381/onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5eec64c0269dcdb8d9a9a53dc4d64f87b9e0c19801d9321246a53b7eb5a7d1bc", size = 13331703 },
268
+ { url = "https://files.pythonhosted.org/packages/12/f1/cefacac137f7bb7bfba57c50c478150fcd3c54aca72762ac2c05ce0532c1/onnxruntime-1.20.1-cp311-cp311-win32.whl", hash = "sha256:a19bc6e8c70e2485a1725b3d517a2319603acc14c1f1a017dda0afe6d4665b41", size = 9813977 },
269
+ { url = "https://files.pythonhosted.org/packages/2c/2d/2d4d202c0bcfb3a4cc2b171abb9328672d7f91d7af9ea52572722c6d8d96/onnxruntime-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:8508887eb1c5f9537a4071768723ec7c30c28eb2518a00d0adcd32c89dea3221", size = 11329895 },
270
+ { url = "https://files.pythonhosted.org/packages/e5/39/9335e0874f68f7d27103cbffc0e235e32e26759202df6085716375c078bb/onnxruntime-1.20.1-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:22b0655e2bf4f2161d52706e31f517a0e54939dc393e92577df51808a7edc8c9", size = 31007580 },
271
+ { url = "https://files.pythonhosted.org/packages/c5/9d/a42a84e10f1744dd27c6f2f9280cc3fb98f869dd19b7cd042e391ee2ab61/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f56e898815963d6dc4ee1c35fc6c36506466eff6d16f3cb9848cea4e8c8172", size = 11952833 },
272
+ { url = "https://files.pythonhosted.org/packages/47/42/2f71f5680834688a9c81becbe5c5bb996fd33eaed5c66ae0606c3b1d6a02/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb71a814f66517a65628c9e4a2bb530a6edd2cd5d87ffa0af0f6f773a027d99e", size = 13333903 },
273
+ { url = "https://files.pythonhosted.org/packages/c8/f1/aabfdf91d013320aa2fc46cf43c88ca0182860ff15df872b4552254a9680/onnxruntime-1.20.1-cp312-cp312-win32.whl", hash = "sha256:bd386cc9ee5f686ee8a75ba74037750aca55183085bf1941da8efcfe12d5b120", size = 9814562 },
274
+ { url = "https://files.pythonhosted.org/packages/dd/80/76979e0b744307d488c79e41051117634b956612cc731f1028eb17ee7294/onnxruntime-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:19c2d843eb074f385e8bbb753a40df780511061a63f9def1b216bf53860223fb", size = 11331482 },
275
+ { url = "https://files.pythonhosted.org/packages/f7/71/c5d980ac4189589267a06f758bd6c5667d07e55656bed6c6c0580733ad07/onnxruntime-1.20.1-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:cc01437a32d0042b606f462245c8bbae269e5442797f6213e36ce61d5abdd8cc", size = 31007574 },
276
+ { url = "https://files.pythonhosted.org/packages/81/0d/13bbd9489be2a6944f4a940084bfe388f1100472f38c07080a46fbd4ab96/onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb44b08e017a648924dbe91b82d89b0c105b1adcfe31e90d1dc06b8677ad37be", size = 11951459 },
277
+ { url = "https://files.pythonhosted.org/packages/c0/ea/4454ae122874fd52bbb8a961262de81c5f932edeb1b72217f594c700d6ef/onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda6aebdf7917c1d811f21d41633df00c58aff2bef2f598f69289c1f1dabc4b3", size = 13331620 },
278
+ { url = "https://files.pythonhosted.org/packages/d8/e0/50db43188ca1c945decaa8fc2a024c33446d31afed40149897d4f9de505f/onnxruntime-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:d30367df7e70f1d9fc5a6a68106f5961686d39b54d3221f760085524e8d38e16", size = 11331758 },
279
+ { url = "https://files.pythonhosted.org/packages/d8/55/3821c5fd60b52a6c82a00bba18531793c93c4addfe64fbf061e235c5617a/onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9158465745423b2b5d97ed25aa7740c7d38d2993ee2e5c3bfacb0c4145c49d8", size = 11950342 },
280
+ { url = "https://files.pythonhosted.org/packages/14/56/fd990ca222cef4f9f4a9400567b9a15b220dee2eafffb16b2adbc55c8281/onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0df6f2df83d61f46e842dbcde610ede27218947c33e994545a22333491e72a3b", size = 13337040 },
281
+ ]
282
+
283
+ [[package]]
284
+ name = "opencv-python"
285
+ version = "4.10.0.84"
286
+ source = { registry = "https://pypi.org/simple" }
287
+ dependencies = [
288
+ { name = "numpy" },
289
+ ]
290
+ sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/b70a2d9ab205110d715906fc8ec83fbb00404aeb3a37a0654fdb68eb0c8c/opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526", size = 95103981 }
291
+ wheels = [
292
+ { url = "https://files.pythonhosted.org/packages/66/82/564168a349148298aca281e342551404ef5521f33fba17b388ead0a84dc5/opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251", size = 54835524 },
293
+ { url = "https://files.pythonhosted.org/packages/64/4a/016cda9ad7cf18c58ba074628a4eaae8aa55f3fd06a266398cef8831a5b9/opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98", size = 56475426 },
294
+ { url = "https://files.pythonhosted.org/packages/81/e4/7a987ebecfe5ceaf32db413b67ff18eb3092c598408862fff4d7cc3fd19b/opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6", size = 41746971 },
295
+ { url = "https://files.pythonhosted.org/packages/3f/a4/d2537f47fd7fcfba966bd806e3ec18e7ee1681056d4b0a9c8d983983e4d5/opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f", size = 62548253 },
296
+ { url = "https://files.pythonhosted.org/packages/1e/39/bbf57e7b9dab623e8773f6ff36385456b7ae7fa9357a5e53db732c347eac/opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236", size = 28737688 },
297
+ { url = "https://files.pythonhosted.org/packages/ec/6c/fab8113424af5049f85717e8e527ca3773299a3c6b02506e66436e19874f/opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe", size = 38842521 },
298
+ ]
299
+
300
+ [[package]]
301
+ name = "packaging"
302
+ version = "24.2"
303
+ source = { registry = "https://pypi.org/simple" }
304
+ sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
305
+ wheels = [
306
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
307
+ ]
308
+
309
+ [[package]]
310
+ name = "protobuf"
311
+ version = "5.29.1"
312
+ source = { registry = "https://pypi.org/simple" }
313
+ sdist = { url = "https://files.pythonhosted.org/packages/d2/4f/1639b7b1633d8fd55f216ba01e21bf2c43384ab25ef3ddb35d85a52033e8/protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb", size = 424965 }
314
+ wheels = [
315
+ { url = "https://files.pythonhosted.org/packages/50/c7/28669b04691a376cf7d0617d612f126aa0fff763d57df0142f9bf474c5b8/protobuf-5.29.1-cp310-abi3-win32.whl", hash = "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110", size = 422706 },
316
+ { url = "https://files.pythonhosted.org/packages/e3/33/dc7a7712f457456b7e0b16420ab8ba1cc8686751d3f28392eb43d0029ab9/protobuf-5.29.1-cp310-abi3-win_amd64.whl", hash = "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34", size = 434505 },
317
+ { url = "https://files.pythonhosted.org/packages/e5/39/44239fb1c6ec557e1731d996a5de89a9eb1ada7a92491fcf9c5d714052ed/protobuf-5.29.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18", size = 417822 },
318
+ { url = "https://files.pythonhosted.org/packages/fb/4a/ec56f101d38d4bef2959a9750209809242d86cf8b897db00f2f98bfa360e/protobuf-5.29.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155", size = 319572 },
319
+ { url = "https://files.pythonhosted.org/packages/04/52/c97c58a33b3d6c89a8138788576d372a90a6556f354799971c6b4d16d871/protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d", size = 319671 },
320
+ { url = "https://files.pythonhosted.org/packages/3b/24/c8c49df8f6587719e1d400109b16c10c6902d0c9adddc8fff82840146f99/protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0", size = 172547 },
321
+ ]
322
+
323
+ [[package]]
324
+ name = "pydantic"
325
+ version = "2.10.3"
326
+ source = { registry = "https://pypi.org/simple" }
327
+ dependencies = [
328
+ { name = "annotated-types" },
329
+ { name = "pydantic-core" },
330
+ { name = "typing-extensions" },
331
+ ]
332
+ sdist = { url = "https://files.pythonhosted.org/packages/45/0f/27908242621b14e649a84e62b133de45f84c255eecb350ab02979844a788/pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9", size = 786486 }
333
+ wheels = [
334
+ { url = "https://files.pythonhosted.org/packages/62/51/72c18c55cf2f46ff4f91ebcc8f75aa30f7305f3d726be3f4ebffb4ae972b/pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d", size = 456997 },
335
+ ]
336
+
337
+ [[package]]
338
+ name = "pydantic-core"
339
+ version = "2.27.1"
340
+ source = { registry = "https://pypi.org/simple" }
341
+ dependencies = [
342
+ { name = "typing-extensions" },
343
+ ]
344
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 }
345
+ wheels = [
346
+ { url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 },
347
+ { url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 },
348
+ { url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 },
349
+ { url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 },
350
+ { url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 },
351
+ { url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 },
352
+ { url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 },
353
+ { url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 },
354
+ { url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 },
355
+ { url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 },
356
+ { url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 },
357
+ { url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 },
358
+ { url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 },
359
+ { url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 },
360
+ { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 },
361
+ { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 },
362
+ { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 },
363
+ { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 },
364
+ { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 },
365
+ { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 },
366
+ { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 },
367
+ { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 },
368
+ { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 },
369
+ { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 },
370
+ { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 },
371
+ { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 },
372
+ { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 },
373
+ { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 },
374
+ { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 },
375
+ { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 },
376
+ { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 },
377
+ { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 },
378
+ { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 },
379
+ { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 },
380
+ { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 },
381
+ { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 },
382
+ { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 },
383
+ { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 },
384
+ { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 },
385
+ { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 },
386
+ { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 },
387
+ { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 },
388
+ ]
389
+
390
+ [[package]]
391
+ name = "pyreadline3"
392
+ version = "3.5.4"
393
+ source = { registry = "https://pypi.org/simple" }
394
+ sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 }
395
+ wheels = [
396
+ { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 },
397
+ ]
398
+
399
+ [[package]]
400
+ name = "python-multipart"
401
+ version = "0.0.19"
402
+ source = { registry = "https://pypi.org/simple" }
403
+ sdist = { url = "https://files.pythonhosted.org/packages/c1/19/93bfb43a3c41b1dd0fa1fa66a08286f6467d36d30297a7aaab8c0b176a26/python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc", size = 36886 }
404
+ wheels = [
405
+ { url = "https://files.pythonhosted.org/packages/e1/f4/ddd0fcdc454cf3870153ae16a818256523d31c3c8136e216bc6836ed4cd1/python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d", size = 24448 },
406
+ ]
407
+
408
+ [[package]]
409
+ name = "redis"
410
+ version = "5.2.1"
411
+ source = { registry = "https://pypi.org/simple" }
412
+ dependencies = [
413
+ { name = "async-timeout", marker = "python_full_version < '3.11.3'" },
414
+ ]
415
+ sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 }
416
+ wheels = [
417
+ { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 },
418
+ ]
419
+
420
+ [[package]]
421
+ name = "ruff"
422
+ version = "0.8.2"
423
+ source = { registry = "https://pypi.org/simple" }
424
+ sdist = { url = "https://files.pythonhosted.org/packages/5e/2b/01245f4f3a727d60bebeacd7ee6d22586c7f62380a2597ddb22c2f45d018/ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5", size = 3349020 }
425
+ wheels = [
426
+ { url = "https://files.pythonhosted.org/packages/91/29/366be70216dba1731a00a41f2f030822b0c96c7c4f3b2c0cdce15cbace74/ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d", size = 10530649 },
427
+ { url = "https://files.pythonhosted.org/packages/63/82/a733956540bb388f00df5a3e6a02467b16c0e529132625fe44ce4c5fb9c7/ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5", size = 10274069 },
428
+ { url = "https://files.pythonhosted.org/packages/3d/12/0b3aa14d1d71546c988a28e1b412981c1b80c8a1072e977a2f30c595cc4a/ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c", size = 9909400 },
429
+ { url = "https://files.pythonhosted.org/packages/23/08/f9f08cefb7921784c891c4151cce6ed357ff49e84b84978440cffbc87408/ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f", size = 10766782 },
430
+ { url = "https://files.pythonhosted.org/packages/e4/71/bf50c321ec179aa420c8ec40adac5ae9cc408d4d37283a485b19a2331ceb/ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897", size = 10286316 },
431
+ { url = "https://files.pythonhosted.org/packages/f2/83/c82688a2a6117539aea0ce63fdf6c08e60fe0202779361223bcd7f40bd74/ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58", size = 11338270 },
432
+ { url = "https://files.pythonhosted.org/packages/7f/d7/bc6a45e5a22e627640388e703160afb1d77c572b1d0fda8b4349f334fc66/ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29", size = 12058579 },
433
+ { url = "https://files.pythonhosted.org/packages/da/3b/64150c93946ec851e6f1707ff586bb460ca671581380c919698d6a9267dc/ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248", size = 11615172 },
434
+ { url = "https://files.pythonhosted.org/packages/e4/9e/cf12b697ea83cfe92ec4509ae414dc4c9b38179cc681a497031f0d0d9a8e/ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93", size = 12882398 },
435
+ { url = "https://files.pythonhosted.org/packages/a9/27/96d10863accf76a9c97baceac30b0a52d917eb985a8ac058bd4636aeede0/ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d", size = 11176094 },
436
+ { url = "https://files.pythonhosted.org/packages/eb/10/cd2fd77d4a4e7f03c29351be0f53278a393186b540b99df68beb5304fddd/ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0", size = 10771884 },
437
+ { url = "https://files.pythonhosted.org/packages/71/5d/beabb2ff18870fc4add05fa3a69a4cb1b1d2d6f83f3cf3ae5ab0d52f455d/ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa", size = 10382535 },
438
+ { url = "https://files.pythonhosted.org/packages/ae/29/6b3fdf3ad3e35b28d87c25a9ff4c8222ad72485ab783936b2b267250d7a7/ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f", size = 10886995 },
439
+ { url = "https://files.pythonhosted.org/packages/e9/dc/859d889b4d9356a1a2cdbc1e4a0dda94052bc5b5300098647e51a58c430b/ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22", size = 11220750 },
440
+ { url = "https://files.pythonhosted.org/packages/0b/08/e8f519f61f1d624264bfd6b8829e4c5f31c3c61193bc3cff1f19dbe7626a/ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1", size = 8729396 },
441
+ { url = "https://files.pythonhosted.org/packages/f8/d4/ba1c7ab72aba37a2b71fe48ab95b80546dbad7a7f35ea28cf66fc5cea5f6/ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea", size = 9594729 },
442
+ { url = "https://files.pythonhosted.org/packages/23/34/db20e12d3db11b8a2a8874258f0f6d96a9a4d631659d54575840557164c8/ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8", size = 9035131 },
443
+ ]
444
+
445
+ [[package]]
446
+ name = "sniffio"
447
+ version = "1.3.1"
448
+ source = { registry = "https://pypi.org/simple" }
449
+ sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
450
+ wheels = [
451
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
452
+ ]
453
+
454
+ [[package]]
455
+ name = "starlette"
456
+ version = "0.41.3"
457
+ source = { registry = "https://pypi.org/simple" }
458
+ dependencies = [
459
+ { name = "anyio" },
460
+ ]
461
+ sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 }
462
+ wheels = [
463
+ { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 },
464
+ ]
465
+
466
+ [[package]]
467
+ name = "super-resolution-api"
468
+ version = "0.1.0"
469
+ source = { virtual = "." }
470
+ dependencies = [
471
+ { name = "dynaconf" },
472
+ { name = "fastapi" },
473
+ { name = "func-timeout" },
474
+ { name = "httpx" },
475
+ { name = "loguru" },
476
+ { name = "numpy" },
477
+ { name = "onnxruntime" },
478
+ { name = "opencv-python" },
479
+ { name = "python-multipart" },
480
+ { name = "redis" },
481
+ { name = "uvicorn" },
482
+ { name = "uvloop" },
483
+ ]
484
+
485
+ [package.dev-dependencies]
486
+ dev = [
487
+ { name = "ruff" },
488
+ ]
489
+
490
+ [package.metadata]
491
+ requires-dist = [
492
+ { name = "dynaconf", specifier = ">=3.2.6" },
493
+ { name = "fastapi", specifier = ">=0.115.6" },
494
+ { name = "func-timeout", specifier = ">=4.3.5" },
495
+ { name = "httpx", specifier = ">=0.28.1" },
496
+ { name = "loguru", specifier = ">=0.7.3" },
497
+ { name = "numpy", specifier = ">=2.1.3" },
498
+ { name = "onnxruntime", specifier = ">=1.20.1" },
499
+ { name = "opencv-python", specifier = ">=4.10.0.84" },
500
+ { name = "python-multipart", specifier = ">=0.0.19" },
501
+ { name = "redis", specifier = ">=5.2.1" },
502
+ { name = "uvicorn", specifier = ">=0.32.1" },
503
+ { name = "uvloop", specifier = ">=0.21.0" },
504
+ ]
505
+
506
+ [package.metadata.requires-dev]
507
+ dev = [{ name = "ruff", specifier = ">=0.8.2" }]
508
+
509
+ [[package]]
510
+ name = "sympy"
511
+ version = "1.13.3"
512
+ source = { registry = "https://pypi.org/simple" }
513
+ dependencies = [
514
+ { name = "mpmath" },
515
+ ]
516
+ sdist = { url = "https://files.pythonhosted.org/packages/11/8a/5a7fd6284fa8caac23a26c9ddf9c30485a48169344b4bd3b0f02fef1890f/sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9", size = 7533196 }
517
+ wheels = [
518
+ { url = "https://files.pythonhosted.org/packages/99/ff/c87e0622b1dadea79d2fb0b25ade9ed98954c9033722eb707053d310d4f3/sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73", size = 6189483 },
519
+ ]
520
+
521
+ [[package]]
522
+ name = "typing-extensions"
523
+ version = "4.12.2"
524
+ source = { registry = "https://pypi.org/simple" }
525
+ sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
526
+ wheels = [
527
+ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
528
+ ]
529
+
530
+ [[package]]
531
+ name = "uvicorn"
532
+ version = "0.32.1"
533
+ source = { registry = "https://pypi.org/simple" }
534
+ dependencies = [
535
+ { name = "click" },
536
+ { name = "h11" },
537
+ ]
538
+ sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 }
539
+ wheels = [
540
+ { url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 },
541
+ ]
542
+
543
+ [[package]]
544
+ name = "uvloop"
545
+ version = "0.21.0"
546
+ source = { registry = "https://pypi.org/simple" }
547
+ sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 }
548
+ wheels = [
549
+ { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 },
550
+ { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 },
551
+ { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 },
552
+ { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 },
553
+ { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 },
554
+ { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 },
555
+ { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 },
556
+ { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 },
557
+ { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 },
558
+ { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 },
559
+ { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 },
560
+ { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 },
561
+ { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 },
562
+ { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 },
563
+ { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 },
564
+ { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 },
565
+ { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 },
566
+ { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 },
567
+ ]
568
+
569
+ [[package]]
570
+ name = "win32-setctime"
571
+ version = "1.1.0"
572
+ source = { registry = "https://pypi.org/simple" }
573
+ sdist = { url = "https://files.pythonhosted.org/packages/6b/dd/f95a13d2b235a28d613ba23ebad55191514550debb968b46aab99f2e3a30/win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2", size = 3676 }
574
+ wheels = [
575
+ { url = "https://files.pythonhosted.org/packages/0a/e6/a7d828fef907843b2a5773ebff47fb79ac0c1c88d60c0ca9530ee941e248/win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad", size = 3604 },
576
+ ]