diff --git a/.gitignore b/.gitignore index 83311ac..08cc81d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ __pycache__/ data/synthetic/ data/classifier/ +data/solver/ +data/real/ + +*.log checkpoints/ onnx_models/ diff --git a/main.py b/main.py deleted file mode 100644 index b76e696..0000000 --- a/main.py +++ /dev/null @@ -1,16 +0,0 @@ -# 这是一个示例 Python 脚本。 - -# 按 Shift+F10 执行或将其替换为您的代码。 -# 按 双击 Shift 在所有地方搜索类、文件、工具窗口、操作和设置。 - - -def print_hi(name): - # 在下面的代码行中使用断点来调试脚本。 - print(f'Hi, {name}') # 按 Ctrl+8 切换断点。 - - -# 按装订区域中的绿色按钮以运行脚本。 -if __name__ == '__main__': - print_hi('PyCharm') - -# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助 diff --git a/pyproject.toml b/pyproject.toml index f770f7a..35f2131 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,9 @@ server = [ cv = [ "opencv-python>=4.8.0", ] +dev = [ + "pytest>=7.0.0", +] [project.scripts] captcha = "cli:main" diff --git a/server.py b/server.py new file mode 100644 index 0000000..03b5270 --- /dev/null +++ b/server.py @@ -0,0 +1,88 @@ +""" +FastAPI HTTP 推理服务 + +提供 REST API 识别验证码: + POST /solve - 上传图片,返回识别结果 + GET /health - 健康检查 + +启动方式: + uv run python cli.py serve --port 8080 + +请求示例: + curl -X POST http://localhost:8080/solve -F "image=@captcha.png" + +响应示例: + {"type": "normal", "result": "A3B8", "confidence": 0.95, "time_ms": 45} +""" + +from __future__ import annotations + + +def create_app(): + """ + 创建 FastAPI 应用实例(工厂函数)。 + + cli.py 的 cmd_serve 依赖此签名。 + 需要安装 server 可选依赖: uv sync --extra server + """ + from fastapi import FastAPI, File, Query, UploadFile + from fastapi.responses import JSONResponse + + from inference.pipeline import CaptchaPipeline + + app = FastAPI( + title="CaptchaBreaker", + description="验证码识别多模型系统 - HTTP 推理服务", + version="0.1.0", + ) + + pipeline: CaptchaPipeline | None = None + + @app.on_event("startup") + def _load_models(): + nonlocal pipeline + try: + pipeline = CaptchaPipeline() + except FileNotFoundError: + # 模型未导出时允许启动,但 /solve 会返回 503 + pipeline = None + + @app.get("/health") + def health(): + models_loaded = pipeline is not None + return {"status": "ok" if models_loaded else "no_models", "models_loaded": models_loaded} + + @app.post("/solve") + async def solve( + image: UploadFile = File(...), + type: str | None = Query(None, description="指定类型跳过分类"), + ): + if pipeline is None: + return JSONResponse( + status_code=503, + content={"error": "模型未加载,请先训练并导出 ONNX 模型"}, + ) + + data = await image.read() + if not data: + return JSONResponse( + status_code=400, + content={"error": "空文件"}, + ) + + try: + result = pipeline.solve(data, captcha_type=type) + except RuntimeError as e: + return JSONResponse( + status_code=400, + content={"error": str(e)}, + ) + + return { + "type": result["type"], + "result": result["result"], + "raw": result["raw"], + "time_ms": result["time_ms"], + } + + return app diff --git a/tests/test_generators.py b/tests/test_generators.py new file mode 100644 index 0000000..4a6dff2 --- /dev/null +++ b/tests/test_generators.py @@ -0,0 +1,173 @@ +""" +测试所有验证码生成器。 + +每种生成器 generate() 1 张 → 验证返回类型、图片尺寸、标签格式。 +""" + +import re + +import pytest +from PIL import Image + +from config import GENERATE_CONFIG, NORMAL_CHARS, MATH_CHARS, THREED_CHARS, SOLVER_CONFIG +from generators import ( + NormalCaptchaGenerator, + MathCaptchaGenerator, + ThreeDCaptchaGenerator, + ThreeDRotateGenerator, + ThreeDSliderGenerator, + SlideDataGenerator, + RotateSolverDataGenerator, +) + + +class TestNormalCaptchaGenerator: + def setup_method(self): + self.gen = NormalCaptchaGenerator(seed=0) + self.cfg = GENERATE_CONFIG["normal"] + + def test_generate_returns_image_and_label(self): + img, label = self.gen.generate() + assert isinstance(img, Image.Image) + assert isinstance(label, str) + + def test_image_size(self): + img, _ = self.gen.generate() + w, h = self.cfg["image_size"] + assert img.size == (w, h) + + def test_label_chars_in_charset(self): + img, label = self.gen.generate() + assert len(label) >= 4 + for ch in label: + assert ch in NORMAL_CHARS, f"char {ch!r} not in NORMAL_CHARS" + + def test_generate_with_text(self): + img, label = self.gen.generate(text="AB12") + assert label == "AB12" + + +class TestMathCaptchaGenerator: + def setup_method(self): + self.gen = MathCaptchaGenerator(seed=0) + self.cfg = GENERATE_CONFIG["math"] + + def test_generate_returns_image_and_label(self): + img, label = self.gen.generate() + assert isinstance(img, Image.Image) + assert isinstance(label, str) + + def test_image_size(self): + img, _ = self.gen.generate() + w, h = self.cfg["image_size"] + assert img.size == (w, h) + + def test_label_is_expression(self): + """Label should be like '3+8' (expression without =? and without result).""" + img, label = self.gen.generate() + assert re.match(r"^\d+[+\-×÷]\d+$", label), f"unexpected label format: {label!r}" + + +class TestThreeDCaptchaGenerator: + def setup_method(self): + self.gen = ThreeDCaptchaGenerator(seed=0) + self.cfg = GENERATE_CONFIG["3d_text"] + + def test_generate_returns_image_and_label(self): + img, label = self.gen.generate() + assert isinstance(img, Image.Image) + assert isinstance(label, str) + + def test_image_size(self): + img, _ = self.gen.generate() + w, h = self.cfg["image_size"] + assert img.size == (w, h) + + def test_label_chars_in_charset(self): + img, label = self.gen.generate() + assert len(label) >= 4 + for ch in label: + assert ch in THREED_CHARS, f"char {ch!r} not in THREED_CHARS" + + +class TestThreeDRotateGenerator: + def setup_method(self): + self.gen = ThreeDRotateGenerator(seed=0) + self.cfg = GENERATE_CONFIG["3d_rotate"] + + def test_generate_returns_image_and_label(self): + img, label = self.gen.generate() + assert isinstance(img, Image.Image) + assert isinstance(label, str) + + def test_image_size(self): + img, _ = self.gen.generate() + w, h = self.cfg["image_size"] + assert img.size == (w, h) + + def test_label_is_angle(self): + img, label = self.gen.generate() + angle = int(label) + assert 0 <= angle <= 359 + + +class TestThreeDSliderGenerator: + def setup_method(self): + self.gen = ThreeDSliderGenerator(seed=0) + self.cfg = GENERATE_CONFIG["3d_slider"] + + def test_generate_returns_image_and_label(self): + img, label = self.gen.generate() + assert isinstance(img, Image.Image) + assert isinstance(label, str) + + def test_image_size(self): + img, _ = self.gen.generate() + w, h = self.cfg["image_size"] + assert img.size == (w, h) + + def test_label_is_offset(self): + img, label = self.gen.generate() + offset = int(label) + lo, hi = self.cfg["gap_x_range"] + assert lo <= offset <= hi + + +class TestSlideDataGenerator: + def setup_method(self): + self.gen = SlideDataGenerator(seed=0) + + def test_generate_returns_image_and_label(self): + img, label = self.gen.generate() + assert isinstance(img, Image.Image) + assert isinstance(label, str) + + def test_image_size(self): + img, _ = self.gen.generate() + h, w = SOLVER_CONFIG["slide"]["cnn_input_size"] + assert img.size == (w, h) + + def test_label_is_numeric(self): + img, label = self.gen.generate() + val = int(label) + assert val >= 0 + + +class TestRotateSolverDataGenerator: + def setup_method(self): + self.gen = RotateSolverDataGenerator(seed=0) + + def test_generate_returns_image_and_label(self): + img, label = self.gen.generate() + assert isinstance(img, Image.Image) + assert isinstance(label, str) + + def test_image_size(self): + img, _ = self.gen.generate() + h, w = SOLVER_CONFIG["rotate"]["input_size"] + assert img.size == (w, h) + + def test_label_is_angle(self): + img, label = self.gen.generate() + angle = int(label) + assert 0 <= angle <= 359 diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..ea0d511 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,177 @@ +""" +测试所有模型前向传播和输出形状。 + +每种模型构造 → forward → 验证输出 shape。 +""" + +import torch +import pytest + +from config import NORMAL_CHARS, MATH_CHARS, THREED_CHARS, IMAGE_SIZE, SOLVER_CONFIG +from models.classifier import CaptchaClassifier +from models.lite_crnn import LiteCRNN +from models.threed_cnn import ThreeDCNN +from models.regression_cnn import RegressionCNN +from models.gap_detector import GapDetectorCNN +from models.rotation_regressor import RotationRegressor + + +class TestCaptchaClassifier: + def setup_method(self): + self.model = CaptchaClassifier(num_types=5) + self.model.eval() + + def test_output_shape(self): + h, w = IMAGE_SIZE["classifier"] + x = torch.randn(2, 1, h, w) + out = self.model(x) + assert out.shape == (2, 5) + + def test_single_batch(self): + h, w = IMAGE_SIZE["classifier"] + x = torch.randn(1, 1, h, w) + out = self.model(x) + assert out.shape == (1, 5) + + def test_param_count_reasonable(self): + n = sum(p.numel() for p in self.model.parameters()) + # Should be < 500KB ≈ 125K float32 params + assert n < 200_000, f"too many params: {n}" + + +class TestLiteCRNN: + def setup_method(self): + self.model = LiteCRNN(chars=NORMAL_CHARS) + self.model.eval() + + def test_output_shape(self): + h, w = IMAGE_SIZE["normal"] + x = torch.randn(2, 1, h, w) + out = self.model(x) + num_classes = len(NORMAL_CHARS) + 1 # +1 for blank + seq_len = w // 4 + assert out.shape == (seq_len, 2, num_classes) + + def test_greedy_decode(self): + h, w = IMAGE_SIZE["normal"] + x = torch.randn(1, 1, h, w) + logits = self.model(x) + decoded = self.model.greedy_decode(logits) + assert isinstance(decoded, list) + assert len(decoded) == 1 + assert isinstance(decoded[0], str) + + def test_param_count_reasonable(self): + n = sum(p.numel() for p in self.model.parameters()) + # Should be < 2MB ≈ 500K float32 params + assert n < 600_000, f"too many params: {n}" + + def test_math_mode(self): + h, w = IMAGE_SIZE["math"] + model = LiteCRNN(chars=MATH_CHARS, img_h=h, img_w=w) + model.eval() + x = torch.randn(1, 1, h, w) + out = model(x) + num_classes = len(MATH_CHARS) + 1 + seq_len = w // 4 + assert out.shape == (seq_len, 1, num_classes) + + +class TestThreeDCNN: + def setup_method(self): + h, w = IMAGE_SIZE["3d_text"] + self.model = ThreeDCNN(chars=THREED_CHARS, img_h=h, img_w=w) + self.model.eval() + + def test_output_shape(self): + h, w = IMAGE_SIZE["3d_text"] + x = torch.randn(2, 1, h, w) + out = self.model(x) + num_classes = len(THREED_CHARS) + 1 + seq_len = w // 4 + assert out.shape == (seq_len, 2, num_classes) + + def test_greedy_decode(self): + h, w = IMAGE_SIZE["3d_text"] + x = torch.randn(1, 1, h, w) + logits = self.model(x) + decoded = self.model.greedy_decode(logits) + assert isinstance(decoded, list) + assert len(decoded) == 1 + + def test_param_count_reasonable(self): + n = sum(p.numel() for p in self.model.parameters()) + # Should be < 5MB ≈ 1.25M float32 params + assert n < 1_500_000, f"too many params: {n}" + + +class TestRegressionCNN: + def test_3d_rotate_shape(self): + h, w = IMAGE_SIZE["3d_rotate"] + model = RegressionCNN(img_h=h, img_w=w) + model.eval() + x = torch.randn(2, 1, h, w) + out = model(x) + assert out.shape == (2, 1) + # Output should be sigmoid [0, 1] + assert out.min() >= 0.0 + assert out.max() <= 1.0 + + def test_3d_slider_shape(self): + h, w = IMAGE_SIZE["3d_slider"] + model = RegressionCNN(img_h=h, img_w=w) + model.eval() + x = torch.randn(2, 1, h, w) + out = model(x) + assert out.shape == (2, 1) + + def test_param_count_reasonable(self): + h, w = IMAGE_SIZE["3d_rotate"] + model = RegressionCNN(img_h=h, img_w=w) + n = sum(p.numel() for p in model.parameters()) + # Should be ~1MB ≈ 250K float32 params + assert n < 400_000, f"too many params: {n}" + + +class TestGapDetectorCNN: + def setup_method(self): + h, w = SOLVER_CONFIG["slide"]["cnn_input_size"] + self.model = GapDetectorCNN(img_h=h, img_w=w) + self.model.eval() + + def test_output_shape(self): + h, w = SOLVER_CONFIG["slide"]["cnn_input_size"] + x = torch.randn(2, 1, h, w) + out = self.model(x) + assert out.shape == (2, 1) + assert out.min() >= 0.0 + assert out.max() <= 1.0 + + def test_param_count_reasonable(self): + n = sum(p.numel() for p in self.model.parameters()) + assert n < 400_000, f"too many params: {n}" + + +class TestRotationRegressor: + def setup_method(self): + h, w = SOLVER_CONFIG["rotate"]["input_size"] + self.model = RotationRegressor(img_h=h, img_w=w) + self.model.eval() + + def test_output_shape(self): + h, w = SOLVER_CONFIG["rotate"]["input_size"] + x = torch.randn(2, 3, h, w) # RGB, 3 channels + out = self.model(x) + assert out.shape == (2, 2) # (sin, cos) + + def test_output_range_tanh(self): + h, w = SOLVER_CONFIG["rotate"]["input_size"] + x = torch.randn(4, 3, h, w) + out = self.model(x) + assert out.min() >= -1.0 + assert out.max() <= 1.0 + + def test_param_count_reasonable(self): + n = sum(p.numel() for p in self.model.parameters()) + # Should be ~2MB ≈ 500K float32 params + assert n < 600_000, f"too many params: {n}" diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py new file mode 100644 index 0000000..eba6bb1 --- /dev/null +++ b/tests/test_pipeline.py @@ -0,0 +1,176 @@ +""" +测试推理流水线组件。 + +- math_eval: 加减乘除正确性 + 异常输入 +- CTC greedy decode (构造 logits) +- SlideSolver (合成图 → OpenCV 检测) +- generate_slide_track 轨迹合理性 +""" + +import math + +import numpy as np +import pytest + +from inference.math_eval import eval_captcha_math +from inference.pipeline import CaptchaPipeline + + +# ============================================================ +# math_eval 测试 +# ============================================================ +class TestMathEval: + def test_addition(self): + assert eval_captcha_math("3+8=?") == "11" + assert eval_captcha_math("12+5") == "17" + assert eval_captcha_math("0+0=?") == "0" + + def test_subtraction(self): + assert eval_captcha_math("15-7=?") == "8" + assert eval_captcha_math("20-20") == "0" + + def test_multiplication(self): + assert eval_captcha_math("12×3=?") == "36" + assert eval_captcha_math("5*4") == "20" + assert eval_captcha_math("6x7") == "42" + assert eval_captcha_math("6X7") == "42" + + def test_division(self): + assert eval_captcha_math("20÷4=?") == "5" + assert eval_captcha_math("9÷3") == "3" + + def test_division_by_zero(self): + with pytest.raises(ValueError, match="除数为零"): + eval_captcha_math("5÷0=?") + + def test_invalid_expression(self): + with pytest.raises(ValueError, match="无法解析"): + eval_captcha_math("abc") + + def test_with_spaces(self): + assert eval_captcha_math("3 + 8 = ?") == "11" + + +# ============================================================ +# CTC greedy decode 测试 +# ============================================================ +class TestCTCGreedyDecode: + """Test the static _ctc_greedy_decode method from CaptchaPipeline.""" + + def test_simple_decode(self): + chars = "ABC" # index 0=blank, 1=A, 2=B, 3=C + T = 6 + C = 4 # blank + 3 chars + logits = np.full((T, 1, C), -10.0, dtype=np.float32) + # Spell out "AB": A, A, blank, B, B, B + logits[0, 0, 1] = 10.0 # A + logits[1, 0, 1] = 10.0 # A (dup, collapsed) + logits[2, 0, 0] = 10.0 # blank + logits[3, 0, 2] = 10.0 # B + logits[4, 0, 2] = 10.0 # B (dup) + logits[5, 0, 2] = 10.0 # B (dup) + result = CaptchaPipeline._ctc_greedy_decode(logits, chars) + assert result == "AB" + + def test_all_blank(self): + chars = "ABC" + T = 5 + C = 4 + logits = np.full((T, 1, C), -10.0, dtype=np.float32) + for t in range(T): + logits[t, 0, 0] = 10.0 + result = CaptchaPipeline._ctc_greedy_decode(logits, chars) + assert result == "" + + def test_repeated_chars_with_blank_separator(self): + chars = "ABC" + T = 5 + C = 4 + logits = np.full((T, 1, C), -10.0, dtype=np.float32) + # Spell "AA": A, blank, A, blank, blank + logits[0, 0, 1] = 10.0 # A + logits[1, 0, 0] = 10.0 # blank + logits[2, 0, 1] = 10.0 # A + logits[3, 0, 0] = 10.0 # blank + logits[4, 0, 0] = 10.0 # blank + result = CaptchaPipeline._ctc_greedy_decode(logits, chars) + assert result == "AA" + + +# ============================================================ +# SlideSolver 测试 +# ============================================================ +class TestSlideSolver: + def test_solve_with_synthetic_image(self): + """Generate a synthetic slide image and verify the solver detects a gap.""" + try: + import cv2 + except ImportError: + pytest.skip("OpenCV not installed") + + from generators.slide_gen import SlideDataGenerator + from solvers.slide_solver import SlideSolver + + gen = SlideDataGenerator(seed=42) + img, label = gen.generate() + expected_gap_x = int(label) + + solver = SlideSolver() + result = solver.solve(img) + + assert "gap_x" in result + assert "gap_x_percent" in result + assert "confidence" in result + assert "method" in result + assert isinstance(result["gap_x"], int) + assert 0.0 <= result["gap_x_percent"] <= 1.0 + + +# ============================================================ +# generate_slide_track 测试 +# ============================================================ +class TestSlideTrack: + def test_track_basic(self): + from utils.slide_utils import generate_slide_track + + track = generate_slide_track(100, seed=42) + assert isinstance(track, list) + assert len(track) >= 10 + + def test_track_point_structure(self): + from utils.slide_utils import generate_slide_track + + track = generate_slide_track(150, seed=0) + for pt in track: + assert "x" in pt + assert "y" in pt + assert "t" in pt + + def test_track_starts_at_origin(self): + from utils.slide_utils import generate_slide_track + + track = generate_slide_track(100, seed=1) + assert track[0]["x"] == 0.0 or abs(track[0]["x"]) < 1e-6 + + def test_track_ends_near_distance(self): + from utils.slide_utils import generate_slide_track + + distance = 120 + track = generate_slide_track(distance, seed=2) + final_x = track[-1]["x"] + assert abs(final_x - distance) < 1.0, f"final x={final_x}, expected ~{distance}" + + def test_track_time_increases(self): + from utils.slide_utils import generate_slide_track + + track = generate_slide_track(100, seed=3) + for i in range(1, len(track)): + assert track[i]["t"] >= track[i - 1]["t"] + + def test_track_y_has_jitter(self): + from utils.slide_utils import generate_slide_track + + track = generate_slide_track(200, seed=4) + y_vals = [pt["y"] for pt in track] + # At least some y values should be non-zero (jitter) + assert any(abs(y) > 0 for y in y_vals) diff --git a/training/__init__.py b/training/__init__.py index 232f3b0..5344949 100644 --- a/training/__init__.py +++ b/training/__init__.py @@ -10,4 +10,6 @@ - train_3d_rotate.py: 训练 3D 旋转回归 (RegressionCNN) - train_3d_slider.py: 训练 3D 滑块回归 (RegressionCNN) - train_classifier.py: 训练调度分类器 (CaptchaClassifier) +- train_slide.py: 训练滑块缺口检测 (GapDetectorCNN) +- train_rotate_solver.py: 训练旋转角度回归 (RotationRegressor) """ diff --git a/training/train_regression_utils.py b/training/train_regression_utils.py index d0a2dd8..adf0106 100644 --- a/training/train_regression_utils.py +++ b/training/train_regression_utils.py @@ -175,10 +175,26 @@ def train_regression_model( best_mae = float("inf") best_tol_acc = 0.0 + start_epoch = 1 ckpt_path = CHECKPOINTS_DIR / f"{model_name}.pth" + # ---- 3.5 断点续训 ---- + if ckpt_path.exists(): + ckpt = torch.load(ckpt_path, map_location=device, weights_only=True) + model.load_state_dict(ckpt["model_state_dict"]) + best_tol_acc = ckpt.get("best_tol_acc", 0.0) + best_mae = ckpt.get("best_mae", float("inf")) + start_epoch = ckpt.get("epoch", 0) + 1 + # 快进 scheduler 到对应 epoch + for _ in range(start_epoch - 1): + scheduler.step() + print( + f"[续训] 从 epoch {start_epoch} 继续, " + f"best_tol_acc={best_tol_acc:.4f}, best_mae={best_mae:.2f}" + ) + # ---- 4. 训练循环 ---- - for epoch in range(1, cfg["epochs"] + 1): + for epoch in range(start_epoch, cfg["epochs"] + 1): model.train() total_loss = 0.0 num_batches = 0 diff --git a/training/train_utils.py b/training/train_utils.py index 7377acc..b22a3c3 100644 --- a/training/train_utils.py +++ b/training/train_utils.py @@ -170,10 +170,25 @@ def train_ctc_model( ctc_loss = nn.CTCLoss(blank=0, zero_infinity=True) best_acc = 0.0 + start_epoch = 1 ckpt_path = CHECKPOINTS_DIR / f"{model_name}.pth" + # ---- 3.5 断点续训 ---- + if ckpt_path.exists(): + ckpt = torch.load(ckpt_path, map_location=device, weights_only=True) + model.load_state_dict(ckpt["model_state_dict"]) + best_acc = ckpt.get("best_acc", 0.0) + start_epoch = ckpt.get("epoch", 0) + 1 + # 快进 scheduler 到对应 epoch + for _ in range(start_epoch - 1): + scheduler.step() + print( + f"[续训] 从 epoch {start_epoch} 继续, " + f"best_acc={best_acc:.4f}" + ) + # ---- 4. 训练循环 ---- - for epoch in range(1, cfg["epochs"] + 1): + for epoch in range(start_epoch, cfg["epochs"] + 1): model.train() total_loss = 0.0 num_batches = 0 @@ -181,11 +196,12 @@ def train_ctc_model( pbar = tqdm(train_loader, desc=f"Epoch {epoch}/{cfg['epochs']}", leave=False) for images, targets, target_lengths, _ in pbar: images = images.to(device) + targets = targets.to(device) + target_lengths = target_lengths.to(device) logits = model(images) # (T, B, C) T, B, C = logits.shape - # cuDNN CTC requires targets/lengths on CPU - input_lengths = torch.full((B,), T, dtype=torch.int32) + input_lengths = torch.full((B,), T, dtype=torch.int32, device=device) log_probs = logits.log_softmax(2) loss = ctc_loss(log_probs, targets, input_lengths, target_lengths) diff --git a/uv.lock b/uv.lock index 2cdf474..0a17990 100644 --- a/uv.lock +++ b/uv.lock @@ -57,6 +57,12 @@ dependencies = [ ] [package.optional-dependencies] +cv = [ + { name = "opencv-python" }, +] +dev = [ + { name = "pytest" }, +] server = [ { name = "fastapi" }, { name = "python-multipart" }, @@ -70,14 +76,16 @@ requires-dist = [ { name = "onnx", specifier = ">=1.14.0" }, { name = "onnxruntime", specifier = ">=1.15.0" }, { name = "onnxscript", specifier = ">=0.6.0" }, + { name = "opencv-python", marker = "extra == 'cv'", specifier = ">=4.8.0" }, { name = "pillow", specifier = ">=10.0.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "python-multipart", marker = "extra == 'server'", specifier = ">=0.0.6" }, { name = "torch", specifier = ">=2.0.0" }, { name = "torchvision", specifier = ">=0.15.0" }, { name = "tqdm", specifier = ">=4.65.0" }, { name = "uvicorn", marker = "extra == 'server'", specifier = ">=0.23.0" }, ] -provides-extras = ["server"] +provides-extras = ["server", "cv", "dev"] [[package]] name = "click" @@ -197,6 +205,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -769,6 +786,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/56/e6b179397497ab93266b6eb00743403a6a699a29063a423c4a14595d3db9/onnxscript-0.6.2-py3-none-any.whl", hash = "sha256:20e3c3fd1da19b3655549d5455a2df719db47374fe430e01e865ae69127c37b9", size = 689064, upload-time = "2026-02-10T22:53:41.663Z" }, ] +[[package]] +name = "opencv-python" +version = "4.13.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/08/ac/6c98c44c650b8114a0fb901691351cfb3956d502e8e9b5cd27f4ee7fbf2f/opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9", size = 32568781, upload-time = "2026-02-05T07:01:41.379Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/82fed528b45173bf629fa44effb76dff8bc9f4eeaee759038362dfa60237/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a", size = 47685527, upload-time = "2026-02-05T06:59:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/90b34a8e2cf9c50fe8ed25cac9011cde0676b4d9d9c973751ac7616223a2/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf", size = 70460872, upload-time = "2026-02-05T06:59:19.162Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616", size = 46708208, upload-time = "2026-02-05T06:59:15.419Z" }, + { url = "https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5", size = 72927042, upload-time = "2026-02-05T06:59:23.389Z" }, + { url = "https://files.pythonhosted.org/packages/fb/17/de5458312bcb07ddf434d7bfcb24bb52c59635ad58c6e7c751b48949b009/opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59", size = 30932638, upload-time = "2026-02-05T07:02:14.98Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -876,6 +912,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "protobuf" version = "7.34.0" @@ -1024,6 +1069,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + [[package]] name = "python-multipart" version = "0.0.22" @@ -1067,6 +1139,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + [[package]] name = "torch" version = "2.10.0"