Align task API and add FunCaptcha support
This commit is contained in:
20
.gitignore
vendored
20
.gitignore
vendored
@@ -7,8 +7,24 @@ __pycache__/
|
||||
|
||||
data/synthetic/
|
||||
data/classifier/
|
||||
data/solver/
|
||||
data/real/
|
||||
data/solver/*
|
||||
!data/solver/slide/
|
||||
!data/solver/slide/real/
|
||||
!data/solver/slide/real/.gitkeep
|
||||
!data/solver/rotate/
|
||||
!data/solver/rotate/real/
|
||||
!data/solver/rotate/real/.gitkeep
|
||||
data/real/*
|
||||
!data/real/normal/
|
||||
!data/real/normal/.gitkeep
|
||||
!data/real/math/
|
||||
!data/real/math/.gitkeep
|
||||
!data/real/3d_text/
|
||||
!data/real/3d_text/.gitkeep
|
||||
!data/real/3d_rotate/
|
||||
!data/real/3d_rotate/.gitkeep
|
||||
!data/real/3d_slider/
|
||||
!data/real/3d_slider/.gitkeep
|
||||
|
||||
*.log
|
||||
|
||||
|
||||
66
AGENTS.md
66
AGENTS.md
@@ -1,36 +1,64 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
Use `cli.py` as the main entrypoint and keep shared settings in `config.py`. `generators/` builds synthetic captchas (5 types: normal, math, 3d_text, 3d_rotate, 3d_slider), `models/` contains the classifier, CTC expert models, and regression models, `training/` owns datasets and training scripts, and `inference/` contains the ONNX pipeline, export code, and math post-processing. Runtime artifacts live in `data/`, `checkpoints/`, and `onnx_models/`.
|
||||
Use `cli.py` as the main command entrypoint, exposed as the `captcha` script from `pyproject.toml`, and keep shared constants in `config.py`. `generators/` contains seven generators: the five captcha generators (`normal`, `math`, `3d_text`, `3d_rotate`, `3d_slider`) plus solver data generators in `slide_gen.py` and `rotate_solver_gen.py`. `models/` contains the classifier, OCR/CTC models, regression models, the two solver models (`gap_detector.py`, `rotation_regressor.py`), and the FunCaptcha Siamese matcher in `fun_captcha_siamese.py`. `training/` owns datasets, shared training utilities, per-model entrypoints, dataset fingerprint helpers in `data_fingerprint.py`, and the FunCaptcha trainer in `train_funcaptcha_rollball.py`. `inference/` contains the ONNX export path, the runtime pipeline, the dedicated FunCaptcha ONNX runner in `fun_captcha.py`, math post-processing, and ONNX sidecar metadata helpers in `model_metadata.py`. `solvers/` implements interactive slide/rotate solving, and `utils/slide_utils.py` generates slider tracks. Runtime artifacts live under `data/synthetic/`, `data/real/`, `data/real/funcaptcha/`, `data/classifier/`, `data/solver/`, `data/server_tasks/`, `checkpoints/`, and `onnx_models/`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
Use `uv` for environment and dependency management.
|
||||
|
||||
- `uv sync` installs the base runtime dependencies from `pyproject.toml`.
|
||||
- `uv sync --extra server` installs HTTP service dependencies.
|
||||
- `uv run captcha generate --type normal --num 1000` generates synthetic training data. Types: `normal`, `math`, `3d_text`, `3d_rotate`, `3d_slider`, `classifier`.
|
||||
- `uv run captcha train --model normal` trains one model; `uv run captcha train --all` runs the full order: `normal -> math -> 3d_text -> 3d_rotate -> 3d_slider -> classifier`.
|
||||
- `uv run captcha export --all` exports all trained models to ONNX.
|
||||
- `uv run captcha export --model 3d_text` exports a single model; `3d_text` is automatically mapped to `threed_text`.
|
||||
- `uv run captcha predict image.png` runs auto-routing inference; add `--type normal` to skip classification.
|
||||
- `uv run captcha predict-dir ./test_images` runs batch inference on a directory.
|
||||
- `uv run captcha serve --port 8080` starts the optional HTTP API when `server.py` is implemented.
|
||||
- `uv sync` installs the base runtime dependencies.
|
||||
- `uv sync --extra server` installs FastAPI service dependencies.
|
||||
- `uv sync --extra cv` installs OpenCV for slide solver workflows.
|
||||
- `uv sync --extra dev` installs pytest.
|
||||
- On Linux `x86_64`, `uv sync` resolves `torch` and `torchvision` from the official PyTorch `cu121` index and pins them to `2.5.1` / `0.20.1`, which has been validated on GTX 1050 Ti (`sm_61`).
|
||||
- Keep `onnxruntime` compatible with Python 3.10 when editing dependencies; the current constraint stays below `1.24`.
|
||||
- `uv run captcha generate --type normal --num 1000` generates captcha training data. Valid types are `normal`, `math`, `3d_text`, `3d_rotate`, `3d_slider`, and `classifier`.
|
||||
- `uv run captcha generate-solver slide --num 30000` and `uv run captcha generate-solver rotate --num 50000` generate solver datasets under `data/solver/`.
|
||||
- `uv run captcha train --model normal` trains one captcha model. `uv run captcha train --all` trains `normal -> math -> 3d_text -> 3d_rotate -> 3d_slider -> classifier`.
|
||||
- `uv run captcha train-solver slide` trains `GapDetectorCNN`; `uv run captcha train-solver rotate` trains `RotationRegressor`.
|
||||
- `uv run captcha train-funcaptcha --question 4_3d_rollball_animals` trains the dedicated FunCaptcha Siamese matcher from full challenge screenshots under `data/real/funcaptcha/4_3d_rollball_animals/`.
|
||||
- `uv run captcha export --all` exports all available ONNX models, including `gap_detector` and `rotation_regressor`, and writes matching `<model>.meta.json` sidecars.
|
||||
- `uv run captcha export --model 3d_text` maps to `threed_text`. The export loader also accepts internal artifact names such as `threed_rotate`, `gap_detector`, `rotation_regressor`, and `funcaptcha_rollball_animals`; `4_3d_rollball_animals` is accepted as an alias for that FunCaptcha artifact.
|
||||
- `uv run captcha predict image.png` runs auto-routing inference. Add `--type normal` to skip classification.
|
||||
- `uv run captcha predict-dir ./test_images` runs batch inference for `.png` and `.jpg` files.
|
||||
- `uv run captcha predict-funcaptcha image.jpg --question 4_3d_rollball_animals` runs the dedicated FunCaptcha matcher and returns `objects`.
|
||||
- `uv run captcha solve slide --bg bg.png [--tpl tpl.png]` runs the slide solver. It uses template matching first when `--tpl` is provided, then OpenCV edge detection, then CNN fallback.
|
||||
- `uv run captcha solve rotate --image img.png` runs the rotate solver.
|
||||
- `uv run captcha serve --host 0.0.0.0 --port 8080` starts the implemented FastAPI service in `server.py`. It supports synchronous `/solve` and `/solve/upload`, plus async task endpoints `/createTask`, `/getTaskResult`, and `/getBalance`, with `/api/v1/*` compatibility aliases. If `CLIENT_KEY` is set in the environment, task endpoints require a matching `clientKey`. `createTask` accepts `callbackUrl`, `softId`, `languagePool`, and optional `task.question`; `task.question=4_3d_rollball_animals` routes to the dedicated FunCaptcha matcher and returns `solution.objects`. `callbackUrl` receives a form-encoded completion callback with configurable retry/backoff in `SERVER_CONFIG`. If `CALLBACK_SIGNING_SECRET` is set, callback requests include HMAC-SHA256 signature headers. Task responses also expose extra `task` / `callback` metadata for async debugging, and task state is persisted under `data/server_tasks/`.
|
||||
- `uv run pytest` runs the test suite.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
Target Python 3.10+ and follow existing style: 4-space indentation, snake_case for functions/modules, PascalCase for classes, and short docstrings on public entrypoints. Keep captcha-type ids exactly `normal`, `math`, `3d_text`, `3d_rotate`, `3d_slider`, and `classifier`. Checkpoint/ONNX file names use `threed_text`, `threed_rotate`, `threed_slider` (underscored, no hyphens). Preserve the design rules from `CLAUDE.md`: float32 training/export, CPU-safe ops, and greedy CTC decoding for OCR models. Regression models (3d_rotate, 3d_slider) output sigmoid [0,1] scaled by `REGRESSION_RANGE`. `normal` uses the local configured charset and currently includes confusing characters; math captchas must be recognized as strings and then evaluated in `inference/math_eval.py`.
|
||||
Target Python 3.10-3.12 and follow the existing style: 4-space indentation, snake_case for functions/modules, PascalCase for classes, and short docstrings on public entrypoints. Keep public captcha type ids exactly `normal`, `math`, `3d_text`, `3d_rotate`, `3d_slider`, and `classifier`. Internal checkpoint/ONNX artifact names use `threed_text`, `threed_rotate`, `threed_slider`, and `funcaptcha_rollball_animals`; solver artifacts are `gap_detector` and `rotation_regressor`. Preserve the design rules from `CLAUDE.md`: float32 training/export, CPU-safe ONNX ops, and greedy CTC decoding for OCR models. `normal` uses `NORMAL_CHARS`, `math` uses `MATH_CHARS` and must be post-processed through `inference/math_eval.py`, and `3d_text` uses `THREED_CHARS`. `3d_rotate` and `3d_slider` output sigmoid values in `[0, 1]` and scale them with `REGRESSION_RANGE`; the rotate solver model outputs `(sin, cos)` on RGB input. The FunCaptcha matcher is a dual-input RGB Siamese model keyed by `task.question`, not by `captchaType`.
|
||||
- Do not casually upgrade `torch` or `torchvision`: newer CUDA 12.8 wheels in this repo's previous environment dropped `sm_61` kernels and failed on GTX 1050 Ti. Re-verify GPU execution before changing the pinned pair.
|
||||
|
||||
## Training & Data Rules
|
||||
- All training scripts must set the global random seed (`random`, `numpy`, `torch`) via `config.RANDOM_SEED` before training begins.
|
||||
- All DataLoaders use `num_workers=0` for cross-platform consistency.
|
||||
- Generator parameters (rotation, noise, shadow, etc.) must come from `config.GENERATE_CONFIG`, not hardcoded values.
|
||||
- `CRNNDataset` emits a `warnings.warn` when a label contains characters outside the configured charset, rather than silently dropping them.
|
||||
- `RegressionDataset` parses numeric labels from filenames and normalizes to [0,1] via `label_range`.
|
||||
- Set the global random seed (`random`, `numpy`, `torch`) from `config.RANDOM_SEED` before training.
|
||||
- Keep `num_workers=0` for all DataLoaders.
|
||||
- Pull generator parameters from `config.GENERATE_CONFIG`, `config.SOLVER_CONFIG`, and related config constants instead of hardcoding them.
|
||||
- Training entrypoints auto-generate missing synthetic data, mix in real data when present, save the best checkpoint to `checkpoints/`, and export a matching ONNX file plus `<model>.meta.json` sidecar to `onnx_models/` at the end.
|
||||
- Synthetic datasets store a `.dataset_meta.json` fingerprint manifest. If generator source or config snapshot changes, training refreshes the synthetic dataset before continuing.
|
||||
- `train_utils.py` and `train_regression_utils.py` only resume checkpoints when the current synthetic dataset fingerprint matches the checkpoint hash. Legacy checkpoints without a stored hash may resume with a warning; refreshed datasets force a restart from epoch 1.
|
||||
- Legacy `normal` and `math` datasets may be adopted into the fingerprint system when no manifest exists, but `math` still validates operator coverage so stale datasets without `÷` samples are regenerated.
|
||||
- `train_classifier.py` prepares a balanced classifier dataset in `data/classifier/<type>/` by symlinking or copying from the current synthetic datasets and rebuilds the derived classifier directories from source data each run.
|
||||
- `CRNNDataset` warns when labels contain characters outside the configured charset instead of silently dropping samples.
|
||||
- `RegressionDataset` parses numeric filename labels and normalizes them to `[0, 1]` using `label_range`.
|
||||
- `RotateSolverDataset` parses angle labels and converts them to `(sin, cos)` targets.
|
||||
- `FunCaptchaChallengeDataset` reads full challenge screenshots from `data/real/funcaptcha/4_3d_rollball_animals/`, crops one reference tile plus `num_candidates` top-row candidates, and trains against the answer index from the filename prefix.
|
||||
- Slide solver training labels are the gap center `x` coordinate, normalized against `SOLVER_CONFIG["slide"]["cnn_input_size"][1]`. All slide solver branches should return the same center-point `gap_x` contract.
|
||||
|
||||
## Data & Testing Guidelines
|
||||
Synthetic generator output should use `{label}_{index:06d}.png`; real labeled samples should use `{label}_{anything}.png`. For regression types, label is the numeric value (angle or offset). Sample targets are defined in `config.py`. Save best checkpoints to `checkpoints/` and export matching ONNX files to `onnx_models/`. Use `pytest`, place tests under `tests/` as `test_<feature>.py`, and run them with `uv run pytest`. For model, data, or routing changes, add a fast smoke test for shapes, decoding, CLI behavior, or pipeline routing.
|
||||
- Synthetic generator output should use `{label}_{index:06d}.png`. OCR real samples should keep `{label}_{anything}.png`.
|
||||
- Regression labels are numeric values in filenames. Captcha regression real data lives under `data/real/3d_rotate/` and `data/real/3d_slider/`; solver real data lives under `data/solver/slide/real/` and `data/solver/rotate/real/`.
|
||||
- FunCaptcha real samples use `{answer_index}_{anything}.png|jpg|jpeg` under `data/real/funcaptcha/4_3d_rollball_animals/`. Each file is the full challenge screenshot, not pre-cropped tiles.
|
||||
- `data/classifier/` is a derived dataset built from per-type captcha samples; do not hand-edit it unless the training flow changes.
|
||||
- ONNX inference should prefer sidecar metadata from `<model>.meta.json` for OCR charset decoding, classifier class order, and regression label ranges, with `config.py` only as a fallback for older exports.
|
||||
- Tests live under `tests/` as `test_<feature>.py`. Current coverage focuses on generators, model output shapes, math evaluation, CTC decoding, slide solving, and slide track generation.
|
||||
- OpenCV-dependent slide solver tests skip automatically when `opencv-python` is not installed. For solver work, prefer `uv sync --extra cv --extra dev`.
|
||||
- FastAPI/httpx-dependent server tests skip automatically when the `server` extra is not installed. For HTTP API work, prefer `uv sync --extra server --extra dev`.
|
||||
- For model, routing, solver, export, or CLI changes, add a fast smoke test that covers shape contracts, decoding behavior, routing, solver fallback, or command behavior.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
Git history is not available in this workspace snapshot, so use short imperative commit subjects such as `Add classifier export smoke test`. Keep pull requests focused, describe affected modules, list the commands you ran, and attach sample outputs when prediction behavior changes.
|
||||
Git history is not available in this workspace snapshot, so use short imperative commit subjects such as `Add slide solver export note`. Keep pull requests focused, describe affected modules, list the commands you ran, and attach sample outputs when prediction or solver behavior changes.
|
||||
|
||||
## Documentation Sync
|
||||
Do not commit large generated datasets unless explicitly required. When a change affects project structure, commands, config, architecture, artifact paths, supported captcha types, or workflow rules, update `AGENTS.md` and `CLAUDE.md` in the same patch.
|
||||
Do not commit large generated datasets unless explicitly required. When a change affects project structure, commands, config, architecture, artifact paths, supported captcha types, or workflow rules, update `AGENTS.md` and `CLAUDE.md` in the same patch. Update `README.md` as well when user-facing commands, solver behavior, or HTTP API behavior changes.
|
||||
|
||||
105
CLAUDE.md
105
CLAUDE.md
@@ -6,13 +6,26 @@
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Python 3.10+
|
||||
- Python 3.10-3.12
|
||||
- uv (包管理,依赖定义在 pyproject.toml)
|
||||
- PyTorch 2.x (训练)
|
||||
- ONNX + ONNXRuntime (推理部署)
|
||||
- Pillow (图像处理)
|
||||
- OpenCV (可选,滑块求解与相关测试)
|
||||
- FastAPI (可选,提供 HTTP 识别服务)
|
||||
|
||||
## 当前命令入口
|
||||
|
||||
- 优先使用 `uv run captcha ...` 调用 CLI;`pyproject.toml` 已将 `captcha` 映射到 `cli:main`
|
||||
- 额外依赖按需安装:
|
||||
- `uv sync --extra server`:HTTP 服务
|
||||
- `uv sync --extra cv`:OpenCV / 滑块 solver
|
||||
- `uv sync --extra dev`:pytest
|
||||
- Linux `x86_64` 环境通过 `pyproject.toml` 的 `tool.uv.sources` / `tool.uv.index` 固定从官方 `cu121` index 安装 `torch==2.5.1` 与 `torchvision==0.20.1`
|
||||
- 该组合已验证可在 GTX 1050 Ti (`sm_61`) 上执行 CUDA;仓库之前解析到的 `torch 2.10.0 + cu128` 不兼容这张卡
|
||||
- Python 3.10 环境下保持 `onnxruntime < 1.24`,避免 `uv` 解析到无 `cp310` wheel 的版本
|
||||
- 当前 CLI 子命令包括:`generate`、`train`、`export`、`predict`、`predict-dir`、`serve`、`generate-solver`、`train-solver`、`solve`、`train-funcaptcha`、`predict-funcaptcha`
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
@@ -32,7 +45,9 @@ captcha-breaker/
|
||||
│ │ ├── math/
|
||||
│ │ ├── 3d_text/
|
||||
│ │ ├── 3d_rotate/
|
||||
│ │ └── 3d_slider/
|
||||
│ │ ├── 3d_slider/
|
||||
│ │ └── funcaptcha/
|
||||
│ │ └── 4_3d_rollball_animals/
|
||||
│ ├── classifier/ # 调度分类器训练数据 (混合各类型)
|
||||
│ └── solver/ # Solver 训练数据
|
||||
│ ├── slide/ # 滑块缺口检测训练数据
|
||||
@@ -54,7 +69,8 @@ captcha-breaker/
|
||||
│ ├── threed_cnn.py # 3D文字验证码专用模型 (更深的CNN)
|
||||
│ ├── regression_cnn.py # 回归CNN (3D旋转+滑块, ~1MB)
|
||||
│ ├── gap_detector.py # 滑块缺口检测CNN (~1MB)
|
||||
│ └── rotation_regressor.py # 旋转角度回归 sin/cos (~2MB)
|
||||
│ ├── rotation_regressor.py # 旋转角度回归 sin/cos (~2MB)
|
||||
│ └── fun_captcha_siamese.py # FunCaptcha 专项 Siamese
|
||||
├── training/
|
||||
│ ├── __init__.py
|
||||
│ ├── train_classifier.py # 训练调度模型
|
||||
@@ -65,13 +81,17 @@ captcha-breaker/
|
||||
│ ├── train_3d_slider.py # 训练3D滑块回归
|
||||
│ ├── train_slide.py # 训练滑块缺口检测
|
||||
│ ├── train_rotate_solver.py # 训练旋转角度回归
|
||||
│ ├── train_funcaptcha_rollball.py # 训练 4_3d_rollball_animals
|
||||
│ ├── train_utils.py # CTC 训练通用逻辑
|
||||
│ ├── train_regression_utils.py # 回归训练通用逻辑
|
||||
│ ├── data_fingerprint.py # 合成数据指纹 / manifest
|
||||
│ └── dataset.py # 通用 Dataset 类
|
||||
├── inference/
|
||||
│ ├── __init__.py
|
||||
│ ├── pipeline.py # 核心推理流水线 (调度+识别)
|
||||
│ ├── fun_captcha.py # FunCaptcha 专项推理
|
||||
│ ├── export_onnx.py # PyTorch → ONNX 导出脚本
|
||||
│ ├── model_metadata.py # ONNX sidecar metadata
|
||||
│ └── math_eval.py # 算式计算模块
|
||||
├── solvers/ # 交互式验证码求解器
|
||||
│ ├── __init__.py
|
||||
@@ -89,16 +109,19 @@ captcha-breaker/
|
||||
│ ├── threed_rotate.pth
|
||||
│ ├── threed_slider.pth
|
||||
│ ├── gap_detector.pth
|
||||
│ └── rotation_regressor.pth
|
||||
│ ├── rotation_regressor.pth
|
||||
│ └── funcaptcha_rollball_animals.pth
|
||||
├── onnx_models/ # 导出的 ONNX 模型
|
||||
│ ├── classifier.onnx
|
||||
│ ├── classifier.meta.json
|
||||
│ ├── normal.onnx
|
||||
│ ├── math.onnx
|
||||
│ ├── threed_text.onnx
|
||||
│ ├── threed_rotate.onnx
|
||||
│ ├── threed_slider.onnx
|
||||
│ ├── gap_detector.onnx
|
||||
│ └── rotation_regressor.onnx
|
||||
│ ├── rotation_regressor.onnx
|
||||
│ └── funcaptcha_rollball_animals.onnx
|
||||
├── server.py # FastAPI 推理服务 (可选)
|
||||
├── cli.py # 命令行入口
|
||||
└── tests/
|
||||
@@ -123,6 +146,10 @@ captcha-breaker/
|
||||
"A3B8" "3+8=?"→11 "X9K2" "135" "87"
|
||||
```
|
||||
|
||||
补充规则:
|
||||
- ONNX 导出时同步生成 `<model>.meta.json`,保存 OCR 字符集、分类器类别顺序、回归标签范围等部署时必需的信息。
|
||||
- `inference/pipeline.py` 优先读取 sidecar metadata;缺失时才回退到 `config.py`,以兼容历史导出产物。
|
||||
|
||||
### 调度分类器 (classifier.py)
|
||||
|
||||
- 任务: 图像分类,判断验证码属于哪个类型
|
||||
@@ -204,6 +231,17 @@ def eval_captcha_math(expr: str) -> str:
|
||||
- 标签范围: 10-200px
|
||||
- 模型体积目标: ~1MB
|
||||
|
||||
### FunCaptcha 专项专家 (fun_captcha_siamese.py)
|
||||
|
||||
- 任务: 识别 `task.question=4_3d_rollball_animals`
|
||||
- 路由方式: 不走调度分类器,而是由 HTTP/CLI 的 `question` 直接路由到专项模型
|
||||
- 输入: 从整张 challenge 截图中裁出 `reference` 和 4 个 top-row candidates
|
||||
- 预处理: RGB `3x48x48`
|
||||
- 架构: 共享编码器 Siamese,`candidate/reference` 特征拼接后输出单个匹配 logit
|
||||
- 训练方式: 每个 challenge 展开成 4 组 pair,正确候选为正样本,其余为负样本
|
||||
- 推理输出: 对 4 个候选分别打分,取 argmax,返回 `objects=[index]`
|
||||
- 模型体积目标: < 2MB
|
||||
|
||||
## 数据生成器规范
|
||||
|
||||
### 基类 (base.py)
|
||||
@@ -234,7 +272,7 @@ class BaseCaptchaGenerator:
|
||||
|
||||
- 生成形如 `A op B = ?` 的算式图片
|
||||
- A, B 范围: 1-30 的整数
|
||||
- op: +, -, × (除法只生成能整除的)
|
||||
- op: +, -, ×, ÷ (除法只生成能整除的)
|
||||
- 确保结果为非负整数
|
||||
- 标签格式: `3+8` (存储算式本身,不存结果)
|
||||
- 视觉风格: 与目标算式验证码一致
|
||||
@@ -259,6 +297,11 @@ class BaseCaptchaGenerator:
|
||||
- 标签 = 缺口 x 坐标偏移(整数字符串)
|
||||
- 文件名格式: `{offset}_{index:06d}.png`
|
||||
|
||||
### Slide Solver 目标约定
|
||||
|
||||
- `slide_gen.py` / `GapDetectorCNN` / `slide_solver.py` 统一使用缺口中心点 `x` 作为输出语义
|
||||
- 训练时先按 solver 输入宽度归一化到 `[0, 1]`,运行时再映射回原图宽度
|
||||
|
||||
## 训练规范
|
||||
|
||||
### 通用训练配置
|
||||
@@ -328,6 +371,11 @@ TRAIN_CONFIG = {
|
||||
7. 训练结束自动导出 ONNX 到 onnx_models/
|
||||
8. DataLoader 统一使用 `num_workers=0` 避免多进程兼容问题
|
||||
|
||||
补充规则:
|
||||
- 合成数据目录写入 `.dataset_meta.json`,其指纹由生成器源码哈希与配置快照共同构成。
|
||||
- OCR / 回归训练只在 checkpoint 中的 `synthetic_data_spec_hash` 与当前数据指纹一致时续训;合成数据被刷新后必须从 epoch 1 重新训练。
|
||||
- legacy `normal` / `math` 数据在 manifest 缺失时可被采纳,但 `math` 仍必须覆盖 `+ - × ÷`,缺失 `÷` 时必须重建数据后再训练。
|
||||
|
||||
### 数据增强策略
|
||||
|
||||
```python
|
||||
@@ -423,16 +471,19 @@ uv run python cli.py train --model 3d_text
|
||||
uv run python cli.py train --model 3d_rotate
|
||||
uv run python cli.py train --model 3d_slider
|
||||
uv run python cli.py train --all # 按依赖顺序全部训练
|
||||
uv run python cli.py train-funcaptcha --question 4_3d_rollball_animals
|
||||
|
||||
# 导出 ONNX
|
||||
uv run python cli.py export --all
|
||||
uv run python cli.py export --model 3d_text # "3d_text" 自动映射为 "threed_text"
|
||||
uv run python cli.py export --model 4_3d_rollball_animals
|
||||
|
||||
# 推理
|
||||
uv run python cli.py predict image.png # 自动分类+识别
|
||||
uv run python cli.py predict image.png --type normal # 跳过分类直接识别
|
||||
uv run python cli.py predict image.png --type 3d_rotate # 指定为旋转类型
|
||||
uv run python cli.py predict-dir ./test_images/ # 批量识别
|
||||
uv run python cli.py predict-funcaptcha challenge.jpg --question 4_3d_rollball_animals
|
||||
|
||||
# 启动 HTTP 服务 (需先安装 server 可选依赖)
|
||||
uv run python cli.py serve --port 8080
|
||||
@@ -443,18 +494,42 @@ uv run python cli.py serve --port 8080
|
||||
纯推理服务,不依赖 torch / 训练代码,仅需 onnxruntime + FastAPI。
|
||||
|
||||
```python
|
||||
# POST /solve - JSON base64 图片识别
|
||||
# POST /solve - JSON base64 图片识别 (同步)
|
||||
# 请求: {"image": "<base64>", "type": "normal"} (type 可选)
|
||||
# 请求也可用 {"image":"<base64>", "question":"4_3d_rollball_animals"}
|
||||
# 响应: {"type": "normal", "result": "A3B8", "raw": "A3B8", "time_ms": 12.3}
|
||||
# FunCaptcha 响应: {"type":"funcaptcha","question":"4_3d_rollball_animals","objects":[2],"result":"2","raw":"2","time_ms":12.3}
|
||||
#
|
||||
# POST /solve/upload - multipart 文件上传识别
|
||||
# 请求: multipart/form-data, 字段名 image, 可选 query param type
|
||||
# POST /solve/upload - multipart 文件上传识别 (同步)
|
||||
# 请求: multipart/form-data, 字段名 image, 可选 query param type/question
|
||||
# 响应: 同上
|
||||
#
|
||||
# POST /createTask - 创建异步识别任务
|
||||
# 请求: {"clientKey":"local","callbackUrl":"https://...","softId":1,"languagePool":"en","task":{"type":"ImageToTextTask","body":"<base64>","captchaType":"normal"}}
|
||||
# 或: {"clientKey":"local","task":{"type":"FunCaptcha","body":"<base64>","question":"4_3d_rollball_animals"}}
|
||||
# 响应: {"errorId":0,"taskId":"<uuid>","status":"processing","createTime":1710000000,"expiresAt":1710000600}
|
||||
#
|
||||
# POST /getTaskResult - 查询异步任务结果
|
||||
# 处理中: {"errorId":0,"taskId":"<uuid>","status":"processing"}
|
||||
# 完成: {"errorId":0,"taskId":"<uuid>","status":"ready","cost":"0.00000","ip":"127.0.0.1","solveCount":1,"task":{"type":"ImageToTextTask","captchaType":"normal"},"callback":{"configured":true,"attempts":1,"delivered":true},"solution":{"text":"A3B8","answer":"A3B8","raw":"A3B8","captchaType":"normal","timeMs":12.3}}
|
||||
# FunCaptcha 完成: {"errorId":0,"taskId":"<uuid>","status":"ready","task":{"type":"FunCaptcha","question":"4_3d_rollball_animals"},"solution":{"objects":[2],"answer":2,"raw":"2","text":"2","question":"4_3d_rollball_animals","timeMs":12.3}}
|
||||
#
|
||||
# POST /getBalance - 本地兼容接口
|
||||
# 响应: {"errorId":0,"balance":999999.0}
|
||||
#
|
||||
# GET /health - 健康检查
|
||||
# 响应: {"status": "ok", "models_loaded": true}
|
||||
# GET /api/v1/health - 健康检查兼容别名
|
||||
# 响应: {"status": "ok", "models_loaded": true, "client_key_required": false, "async_tasks": {...}}
|
||||
```
|
||||
|
||||
- 异步任务接口参考 `ohmycaptcha` 的 `taskId` 轮询模式实现
|
||||
- 兼容根路径与 `/api/v1/*` 双路由;如设置环境变量 `CLIENT_KEY`,则任务接口要求请求体中的 `clientKey` 匹配
|
||||
- 普通 OCR 任务通过 `task.captchaType` 路由;FunCaptcha 专项任务通过 `task.question` 路由,不进入 `CaptchaPipeline` 调度分类器
|
||||
- `callbackUrl` 会在任务完成后触发一次 `application/x-www-form-urlencoded` POST 回调,字段包含 `id/taskId` 与 `code`;默认失败重试 2 次并按退避间隔重发
|
||||
- 若设置环境变量 `CALLBACK_SIGNING_SECRET`,回调请求会携带 `X-CaptchaBreaker-Timestamp`、`X-CaptchaBreaker-Signature-Alg` 和 `X-CaptchaBreaker-Signature` 头,签名算法为 `hmac-sha256`
|
||||
- 任务结果额外暴露 `task` / `callback` 元信息,便于接入方排查异步状态
|
||||
- 任务结果持久化在 `data/server_tasks/`,默认 TTL 为 600 秒,服务重启后可恢复未过期任务
|
||||
|
||||
## 关键约束和注意事项
|
||||
|
||||
1. **所有模型用 float32 训练,导出 ONNX 时不做量化**,先保证精度
|
||||
@@ -466,7 +541,7 @@ uv run python cli.py serve --port 8080
|
||||
11. **数据集字符过滤**: `CRNNDataset` 加载标签时,若发现字符不在字符集内会发出 warning,便于排查标注/字符集不匹配问题
|
||||
7. **模型保存格式**: CTC checkpoint 包含 model_state_dict, chars, best_acc, epoch; 回归 checkpoint 包含 model_state_dict, label_range, best_mae, best_tol_acc, epoch
|
||||
8. **不使用 GPU 特有功能**,确保 CPU 也能训练和推理 (只是慢一些)
|
||||
9. **类型扩展**: 新增验证码类型时,只需 (1) 加生成器 (2) 加专家模型 (3) 调度器加一个类别重新训练
|
||||
9. **类型扩展**: OCR/回归验证码类型继续遵循 “生成器 + 专家模型 + 分类器类别” 的主线;FunCaptcha 这类专项 challenge 优先走 `task.question` 专项路由,不强行塞进 `CAPTCHA_TYPES`
|
||||
10. **文档同步**: 对项目结构、配置、架构等做出变更时,必须同步更新 CLAUDE.md 中的对应内容,保持文档与代码一致
|
||||
|
||||
## 目标指标
|
||||
@@ -479,6 +554,7 @@ uv run python cli.py serve --port 8080
|
||||
| 3D立体文字 | > 85% | < 50ms | < 5MB |
|
||||
| 3D旋转 (±5°) | > 85% | < 30ms | ~1MB |
|
||||
| 3D滑块 (±3px) | > 90% | < 30ms | ~1MB |
|
||||
| FunCaptcha rollball | > 90% challenge acc | < 30ms | < 2MB |
|
||||
| 全流水线 | - | < 80ms | < 12MB 总计 |
|
||||
|
||||
## 开发顺序
|
||||
@@ -488,9 +564,10 @@ uv run python cli.py serve --port 8080
|
||||
3. 实现 training/dataset.py 通用数据集类
|
||||
4. 按顺序训练: normal → math → 3d_text → 3d_rotate → 3d_slider → classifier
|
||||
5. 实现 inference/pipeline.py 和 export_onnx.py
|
||||
6. 实现 cli.py 统一入口
|
||||
7. 可选: server.py HTTP 服务
|
||||
8. 编写 tests/
|
||||
6. 实现 FunCaptcha 专项推理/训练支线 (`fun_captcha.py`, `train_funcaptcha_rollball.py`)
|
||||
7. 实现 cli.py 统一入口
|
||||
8. 可选: server.py HTTP 服务
|
||||
9. 编写 tests/
|
||||
|
||||
## 交互式 Solver 扩展
|
||||
|
||||
|
||||
219
README.md
219
README.md
@@ -30,9 +30,15 @@
|
||||
|
||||
| 类型 | 模型 | 说明 |
|
||||
|------|------|------|
|
||||
| slide | GapDetectorCNN | 滑块缺口检测 (OpenCV 优先 + CNN 兜底) |
|
||||
| slide | GapDetectorCNN | 滑块缺口检测 (统一输出缺口中心 x,OpenCV 优先 + CNN 兜底) |
|
||||
| rotate | RotationRegressor | 旋转角度回归 (sin/cos 编码) |
|
||||
|
||||
### FunCaptcha 专项
|
||||
|
||||
| question | 模型 | 说明 |
|
||||
|------|------|------|
|
||||
| 4_3d_rollball_animals | FunCaptchaSiamese | 整张 challenge 图裁切后做 reference/candidate 配对打分,返回 `objects` |
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
@@ -49,81 +55,116 @@ uv sync --extra cv
|
||||
uv sync --extra dev
|
||||
```
|
||||
|
||||
说明:
|
||||
- 项目当前通过 `pyproject.toml` 将 `onnxruntime` 约束在 `<1.24`,以保持 Python 3.10 环境下的 `uv` 可安装性。
|
||||
- Linux `x86_64` 环境下,`uv sync` 会从官方 PyTorch `cu121` index 安装 `torch==2.5.1` 和 `torchvision==0.20.1`。这组版本已验证可在 GTX 1050 Ti (`sm_61`) 上执行 CUDA。
|
||||
- 仓库之前自动解析到的 `torch 2.10 + cu128` 对 GTX 1050 Ti 不兼容;如果后续升级 `torch`,先重新验证 GPU 实际能执行 CUDA 张量运算。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 生成训练数据
|
||||
|
||||
```bash
|
||||
python cli.py generate --type normal --num 60000
|
||||
python cli.py generate --type math --num 60000
|
||||
python cli.py generate --type 3d_text --num 80000
|
||||
python cli.py generate --type 3d_rotate --num 60000
|
||||
python cli.py generate --type 3d_slider --num 60000
|
||||
python cli.py generate --type classifier --num 50000
|
||||
uv run captcha generate --type normal --num 60000
|
||||
uv run captcha generate --type math --num 60000
|
||||
uv run captcha generate --type 3d_text --num 80000
|
||||
uv run captcha generate --type 3d_rotate --num 60000
|
||||
uv run captcha generate --type 3d_slider --num 60000
|
||||
uv run captcha generate --type classifier --num 50000
|
||||
```
|
||||
|
||||
### 2. 训练模型
|
||||
|
||||
```bash
|
||||
# 逐个训练
|
||||
python -m training.train_normal
|
||||
python -m training.train_math
|
||||
python -m training.train_3d_text
|
||||
python -m training.train_3d_rotate
|
||||
python -m training.train_3d_slider
|
||||
python -m training.train_classifier
|
||||
uv run captcha train --model normal
|
||||
uv run captcha train --model math
|
||||
uv run captcha train --model 3d_text
|
||||
uv run captcha train --model 3d_rotate
|
||||
uv run captcha train --model 3d_slider
|
||||
uv run captcha train --model classifier
|
||||
|
||||
# 或通过 CLI 一键训练
|
||||
python cli.py train --all
|
||||
uv run captcha train --all
|
||||
```
|
||||
|
||||
训练支持断点续训:检测到已有 checkpoint 会自动从上次中断处继续。
|
||||
OCR / 回归训练在合成数据指纹与 checkpoint 一致时支持断点续训;生成规则变化会自动刷新数据并从 epoch 1 重新训练。分类器和 rotate solver 当前仍按整轮训练处理。
|
||||
|
||||
### 3. 导出 ONNX
|
||||
|
||||
```bash
|
||||
python cli.py export --all
|
||||
uv run captcha export --all
|
||||
# 或单个导出
|
||||
python cli.py export --model normal
|
||||
uv run captcha export --model normal
|
||||
uv run captcha export --model 4_3d_rollball_animals
|
||||
```
|
||||
|
||||
导出会同时生成 `<model>.meta.json` sidecar,保存 OCR 字符集、分类器类别顺序、回归标签范围或 FunCaptcha challenge 裁切元信息,部署推理优先读取这些 metadata。
|
||||
|
||||
### 4. 推理
|
||||
|
||||
```bash
|
||||
# 单张识别 (自动分类 + 识别)
|
||||
python cli.py predict image.png
|
||||
uv run captcha predict image.png
|
||||
|
||||
# 指定类型跳过分类
|
||||
python cli.py predict image.png --type normal
|
||||
uv run captcha predict image.png --type normal
|
||||
|
||||
# 批量识别
|
||||
python cli.py predict-dir ./test_images/
|
||||
uv run captcha predict-dir ./test_images/
|
||||
|
||||
# FunCaptcha 专项识别
|
||||
uv run captcha predict-funcaptcha challenge.jpg --question 4_3d_rollball_animals
|
||||
```
|
||||
|
||||
### 5. 交互式 Solver
|
||||
|
||||
```bash
|
||||
# 生成 Solver 训练数据
|
||||
python cli.py generate-solver slide --num 30000
|
||||
python cli.py generate-solver rotate --num 50000
|
||||
uv run captcha generate-solver slide --num 30000
|
||||
uv run captcha generate-solver rotate --num 50000
|
||||
|
||||
# 训练
|
||||
python cli.py train-solver slide
|
||||
python cli.py train-solver rotate
|
||||
uv run captcha train-solver slide
|
||||
uv run captcha train-solver rotate
|
||||
|
||||
# 求解
|
||||
python cli.py solve slide --bg bg.png --tpl tpl.png
|
||||
python cli.py solve rotate --image img.png
|
||||
uv run captcha solve slide --bg bg.png --tpl tpl.png
|
||||
uv run captcha solve rotate --image img.png
|
||||
```
|
||||
|
||||
### 6. FunCaptcha 专项训练
|
||||
|
||||
准备整张 challenge 标注图到 `data/real/funcaptcha/4_3d_rollball_animals/`,文件名前缀为正确候选索引,例如 `2_demo.jpg`。
|
||||
|
||||
```bash
|
||||
uv run captcha train-funcaptcha --question 4_3d_rollball_animals
|
||||
uv run captcha export --model 4_3d_rollball_animals
|
||||
uv run captcha predict-funcaptcha challenge.jpg --question 4_3d_rollball_animals
|
||||
```
|
||||
|
||||
## HTTP API
|
||||
|
||||
```bash
|
||||
uv sync --extra server
|
||||
python cli.py serve --port 8080
|
||||
uv run captcha serve --port 8080
|
||||
```
|
||||
|
||||
### POST /solve — base64 图片识别
|
||||
如需和 `ohmycaptcha` / YesCaptcha 风格客户端对齐,可在启动前设置 `CLIENT_KEY`:
|
||||
|
||||
```bash
|
||||
CLIENT_KEY=local uv run captcha serve --port 8080
|
||||
```
|
||||
|
||||
如需让回调接收方校验来源,可再设置 `CALLBACK_SIGNING_SECRET`;服务会在回调请求头里附带 HMAC-SHA256 签名:
|
||||
|
||||
```bash
|
||||
CLIENT_KEY=local CALLBACK_SIGNING_SECRET=shared-secret uv run captcha serve --port 8080
|
||||
```
|
||||
|
||||
同步/异步接口都提供根路径和 `/api/v1/*` 兼容别名,例如 `/solve` 与 `/api/v1/solve`、`/createTask` 与 `/api/v1/createTask` 都可用。
|
||||
|
||||
### POST /solve — base64 图片识别(同步)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/solve \
|
||||
@@ -142,6 +183,17 @@ curl -X POST http://localhost:8080/solve \
|
||||
|
||||
`type` 可选,省略则自动分类。可选值:`normal` / `math` / `3d_text` / `3d_rotate` / `3d_slider`
|
||||
|
||||
如需专项 FunCaptcha 路由,可额外传 `question`,例如:
|
||||
|
||||
```json
|
||||
{
|
||||
"image": "<base64 编码的图片>",
|
||||
"question": "4_3d_rollball_animals"
|
||||
}
|
||||
```
|
||||
|
||||
此时响应会额外包含 `objects`。
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
@@ -153,17 +205,118 @@ curl -X POST http://localhost:8080/solve \
|
||||
}
|
||||
```
|
||||
|
||||
### POST /solve/upload — 文件上传识别
|
||||
### POST /solve/upload — 文件上传识别(同步)
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/solve/upload?type=normal" \
|
||||
-F "image=@captcha.png"
|
||||
```
|
||||
|
||||
### GET /health — 健康检查
|
||||
### POST /createTask — 创建异步识别任务
|
||||
|
||||
接口风格参考 `ohmycaptcha` 的 `taskId` 轮询方案,适合需要统一异步协议的接入方。任务结果会持久化到 `data/server_tasks/`,服务重启后仍可继续查询,默认保留 10 分钟;如设置了 `CLIENT_KEY`,则 `clientKey` 必须匹配。`callbackUrl`、`softId`、`languagePool` 字段可传入,其中 `callbackUrl` 会在任务完成后收到一次 `application/x-www-form-urlencoded` POST 回调;默认失败重试 2 次,可通过 `SERVER_CONFIG` 调整超时、重试次数和退避间隔。如设置了 `CALLBACK_SIGNING_SECRET`,回调还会带上 `X-CaptchaBreaker-Timestamp`、`X-CaptchaBreaker-Signature-Alg`、`X-CaptchaBreaker-Signature`。普通 OCR 任务走 `task.captchaType`,专项 FunCaptcha 任务走 `task.question`。
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/createTask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"clientKey":"local","task":{"type":"ImageToTextTask","body":"'"$(base64 -w0 captcha.png)"'","captchaType":"normal"}}'
|
||||
```
|
||||
|
||||
FunCaptcha 示例:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/createTask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"clientKey":"local","task":{"type":"FunCaptcha","body":"'"$(base64 -w0 challenge.jpg)"'","question":"4_3d_rollball_animals"}}'
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{"status": "ok", "models_loaded": true}
|
||||
{
|
||||
"errorId": 0,
|
||||
"taskId": "4ec6f1904da2446caa6c6313c0f7d2b0",
|
||||
"status": "processing",
|
||||
"createTime": 1710000000,
|
||||
"expiresAt": 1710000600
|
||||
}
|
||||
```
|
||||
|
||||
### POST /getTaskResult — 查询异步任务结果
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/getTaskResult \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"clientKey":"local","taskId":"4ec6f1904da2446caa6c6313c0f7d2b0"}'
|
||||
```
|
||||
|
||||
处理中:
|
||||
|
||||
```json
|
||||
{
|
||||
"errorId": 0,
|
||||
"taskId": "4ec6f1904da2446caa6c6313c0f7d2b0",
|
||||
"status": "processing",
|
||||
"createTime": 1710000000
|
||||
}
|
||||
```
|
||||
|
||||
完成:
|
||||
|
||||
```json
|
||||
{
|
||||
"errorId": 0,
|
||||
"taskId": "4ec6f1904da2446caa6c6313c0f7d2b0",
|
||||
"status": "ready",
|
||||
"cost": "0.00000",
|
||||
"ip": "127.0.0.1",
|
||||
"createTime": 1710000000,
|
||||
"endTime": 1710000001,
|
||||
"expiresAt": 1710000600,
|
||||
"solveCount": 1,
|
||||
"task": {
|
||||
"type": "ImageToTextTask",
|
||||
"captchaType": "normal"
|
||||
},
|
||||
"callback": {
|
||||
"configured": true,
|
||||
"url": "https://example.com/callback",
|
||||
"attempts": 1,
|
||||
"delivered": true,
|
||||
"deliveredAt": 1710000001,
|
||||
"lastError": null
|
||||
},
|
||||
"solution": {
|
||||
"text": "A3B8",
|
||||
"answer": "A3B8",
|
||||
"raw": "A3B8",
|
||||
"captchaType": "normal",
|
||||
"timeMs": 12.3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /getBalance — 本地兼容接口
|
||||
|
||||
```json
|
||||
{"errorId": 0, "balance": 999999.0}
|
||||
```
|
||||
|
||||
### GET /health 或 /api/v1/health — 健康检查
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"models_loaded": true,
|
||||
"client_key_required": false,
|
||||
"async_tasks": {
|
||||
"active": 0,
|
||||
"processing": 0,
|
||||
"ready": 0,
|
||||
"failed": 0,
|
||||
"ttl_seconds": 600
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
@@ -188,11 +341,13 @@ curl -X POST "http://localhost:8080/solve/upload?type=normal" \
|
||||
│ ├── gap_detector.py # 滑块缺口检测
|
||||
│ └── rotation_regressor.py # 旋转角度回归
|
||||
├── training/ # 训练脚本
|
||||
│ ├── data_fingerprint.py # 合成数据指纹 / manifest
|
||||
│ ├── train_utils.py # CTC 训练通用逻辑
|
||||
│ ├── train_regression_utils.py # 回归训练通用逻辑
|
||||
│ ├── dataset.py # 通用 Dataset 类
|
||||
│ └── train_*.py # 各模型训练入口
|
||||
├── inference/ # 推理 (仅依赖 onnxruntime)
|
||||
│ ├── model_metadata.py # ONNX sidecar metadata
|
||||
│ ├── pipeline.py # 核心推理流水线
|
||||
│ ├── export_onnx.py # ONNX 导出
|
||||
│ └── math_eval.py # 算式计算
|
||||
@@ -226,7 +381,7 @@ python -m pytest tests/ -v
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Python 3.10+
|
||||
- Python 3.10-3.12
|
||||
- PyTorch 2.x (训练)
|
||||
- ONNX + ONNXRuntime (推理部署)
|
||||
- FastAPI + uvicorn (HTTP 服务)
|
||||
|
||||
68
cli.py
68
cli.py
@@ -18,6 +18,8 @@ CaptchaBreaker 命令行入口
|
||||
python cli.py train-solver rotate
|
||||
python cli.py solve slide --bg bg.png [--tpl tpl.png]
|
||||
python cli.py solve rotate --image img.png
|
||||
python cli.py train-funcaptcha --question 4_3d_rollball_animals
|
||||
python cli.py predict-funcaptcha image.jpg --question 4_3d_rollball_animals
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -125,6 +127,7 @@ def cmd_export(args):
|
||||
"3d_text": "threed_text",
|
||||
"3d_rotate": "threed_rotate",
|
||||
"3d_slider": "threed_slider",
|
||||
"4_3d_rollball_animals": "funcaptcha_rollball_animals",
|
||||
}
|
||||
name = alias.get(args.model, args.model)
|
||||
_load_and_export(name)
|
||||
@@ -284,6 +287,52 @@ def cmd_solve(args):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_train_funcaptcha(args):
|
||||
"""训练 FunCaptcha 专项模型。"""
|
||||
from config import FUN_CAPTCHA_TASKS
|
||||
from training.train_funcaptcha_rollball import main as train_rollball
|
||||
|
||||
question = args.question
|
||||
if question not in FUN_CAPTCHA_TASKS:
|
||||
print(f"未知 FunCaptcha question: {question} 可选: {', '.join(FUN_CAPTCHA_TASKS)}")
|
||||
sys.exit(1)
|
||||
|
||||
if question == "4_3d_rollball_animals":
|
||||
train_rollball(question=question)
|
||||
return
|
||||
|
||||
print(f"暂未实现该 FunCaptcha 训练入口: {question}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_predict_funcaptcha(args):
|
||||
"""专项 FunCaptcha 预测。"""
|
||||
from config import FUN_CAPTCHA_TASKS
|
||||
from inference.fun_captcha import FunCaptchaRollballPipeline
|
||||
|
||||
image_path = args.image
|
||||
question = args.question
|
||||
if not Path(image_path).exists():
|
||||
print(f"文件不存在: {image_path}")
|
||||
sys.exit(1)
|
||||
if question not in FUN_CAPTCHA_TASKS:
|
||||
print(f"未知 FunCaptcha question: {question} 可选: {', '.join(FUN_CAPTCHA_TASKS)}")
|
||||
sys.exit(1)
|
||||
|
||||
if question == "4_3d_rollball_animals":
|
||||
pipeline = FunCaptchaRollballPipeline(question=question)
|
||||
else:
|
||||
print(f"暂未实现该 FunCaptcha 预测入口: {question}")
|
||||
sys.exit(1)
|
||||
|
||||
result = pipeline.solve(image_path)
|
||||
print(f"文件: {image_path}")
|
||||
print(f"question: {result['question']}")
|
||||
print(f"objects: {result['objects']}")
|
||||
print(f"result: {result['result']}")
|
||||
print(f"耗时: {result['time_ms']:.1f} ms")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="captcha-breaker",
|
||||
@@ -352,6 +401,23 @@ def main():
|
||||
p_solve.add_argument("--tpl", default=None, help="模板图路径 (slide 可选)")
|
||||
p_solve.add_argument("--image", help="图片路径 (rotate 必需)")
|
||||
|
||||
# ---- train-funcaptcha ----
|
||||
p_train_fun = subparsers.add_parser("train-funcaptcha", help="训练 FunCaptcha 专项模型")
|
||||
p_train_fun.add_argument(
|
||||
"--question",
|
||||
required=True,
|
||||
help="专项 question,如: 4_3d_rollball_animals",
|
||||
)
|
||||
|
||||
# ---- predict-funcaptcha ----
|
||||
p_pred_fun = subparsers.add_parser("predict-funcaptcha", help="识别单张 FunCaptcha challenge")
|
||||
p_pred_fun.add_argument("image", help="图片路径")
|
||||
p_pred_fun.add_argument(
|
||||
"--question",
|
||||
required=True,
|
||||
help="专项 question,如: 4_3d_rollball_animals",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command is None:
|
||||
@@ -368,6 +434,8 @@ def main():
|
||||
"generate-solver": cmd_generate_solver,
|
||||
"train-solver": cmd_train_solver,
|
||||
"solve": cmd_solve,
|
||||
"train-funcaptcha": cmd_train_funcaptcha,
|
||||
"predict-funcaptcha": cmd_predict_funcaptcha,
|
||||
}
|
||||
|
||||
cmd_map[args.command](args)
|
||||
|
||||
48
config.py
48
config.py
@@ -19,6 +19,7 @@ DATA_DIR = PROJECT_ROOT / "data"
|
||||
SYNTHETIC_DIR = DATA_DIR / "synthetic"
|
||||
REAL_DIR = DATA_DIR / "real"
|
||||
CLASSIFIER_DIR = DATA_DIR / "classifier"
|
||||
SERVER_TASKS_DIR = DATA_DIR / "server_tasks"
|
||||
|
||||
# 合成数据子目录
|
||||
SYNTHETIC_NORMAL_DIR = SYNTHETIC_DIR / "normal"
|
||||
@@ -33,6 +34,8 @@ REAL_MATH_DIR = REAL_DIR / "math"
|
||||
REAL_3D_TEXT_DIR = REAL_DIR / "3d_text"
|
||||
REAL_3D_ROTATE_DIR = REAL_DIR / "3d_rotate"
|
||||
REAL_3D_SLIDER_DIR = REAL_DIR / "3d_slider"
|
||||
REAL_FUN_CAPTCHA_DIR = REAL_DIR / "funcaptcha"
|
||||
REAL_FUN_CAPTCHA_ROLLBALL_DIR = REAL_FUN_CAPTCHA_DIR / "4_3d_rollball_animals"
|
||||
|
||||
# Solver 数据目录
|
||||
SOLVER_DATA_DIR = DATA_DIR / "solver"
|
||||
@@ -51,8 +54,10 @@ for _dir in [
|
||||
SYNTHETIC_3D_TEXT_DIR, SYNTHETIC_3D_ROTATE_DIR, SYNTHETIC_3D_SLIDER_DIR,
|
||||
REAL_NORMAL_DIR, REAL_MATH_DIR,
|
||||
REAL_3D_TEXT_DIR, REAL_3D_ROTATE_DIR, REAL_3D_SLIDER_DIR,
|
||||
REAL_FUN_CAPTCHA_DIR, REAL_FUN_CAPTCHA_ROLLBALL_DIR,
|
||||
CLASSIFIER_DIR, CHECKPOINTS_DIR, ONNX_DIR,
|
||||
SLIDE_DATA_DIR, ROTATE_SOLVER_DATA_DIR,
|
||||
SERVER_TASKS_DIR,
|
||||
]:
|
||||
_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -82,6 +87,7 @@ IMAGE_SIZE = {
|
||||
"3d_text": (60, 160), # 3D 立体文字识别
|
||||
"3d_rotate": (80, 80), # 3D 旋转角度回归 (正方形)
|
||||
"3d_slider": (80, 240), # 3D 滑块偏移回归
|
||||
"funcaptcha_rollball_animals": (48, 48), # FunCaptcha 专项 Siamese 输入
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
@@ -99,7 +105,7 @@ GENERATE_CONFIG = {
|
||||
},
|
||||
"math": {
|
||||
"operand_range": (1, 30), # 操作数范围
|
||||
"operators": ["+", "-", "×"], # 支持的运算符 (除法只生成能整除的)
|
||||
"operators": ["+", "-", "×", "÷"], # 支持的运算符 (除法只生成能整除的)
|
||||
"image_size": (160, 40), # 生成图片尺寸 (W, H)
|
||||
"bg_color_range": (230, 255),
|
||||
"rotation_range": (-10, 10),
|
||||
@@ -184,6 +190,14 @@ TRAIN_CONFIG = {
|
||||
"loss": "SmoothL1",
|
||||
"val_split": 0.1,
|
||||
},
|
||||
"funcaptcha_rollball_animals": {
|
||||
"epochs": 30,
|
||||
"batch_size": 64,
|
||||
"lr": 1e-3,
|
||||
"scheduler": "cosine",
|
||||
"loss": "BCEWithLogits",
|
||||
"val_split": 0.1,
|
||||
},
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
@@ -226,6 +240,23 @@ INFERENCE_CONFIG = {
|
||||
"normalize_std": 0.5,
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# FunCaptcha 专项任务配置
|
||||
# ============================================================
|
||||
FUN_CAPTCHA_TASKS = {
|
||||
"4_3d_rollball_animals": {
|
||||
"artifact_name": "funcaptcha_rollball_animals",
|
||||
"checkpoint_name": "funcaptcha_rollball_animals",
|
||||
"data_dir": REAL_FUN_CAPTCHA_ROLLBALL_DIR,
|
||||
"input_size": IMAGE_SIZE["funcaptcha_rollball_animals"], # (H, W)
|
||||
"tile_size": (200, 200), # (W, H)
|
||||
"reference_box": (0, 200, 200, 400), # (x1, y1, x2, y2)
|
||||
"num_candidates": 4,
|
||||
"answer_index_base": 0,
|
||||
"channels": 3,
|
||||
},
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 随机种子 (保证数据生成可复现)
|
||||
# ============================================================
|
||||
@@ -246,6 +277,18 @@ def get_device():
|
||||
SERVER_CONFIG = {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8080,
|
||||
"task_ttl_seconds": 600,
|
||||
"task_workers": 2,
|
||||
"tasks_dir": str(SERVER_TASKS_DIR),
|
||||
"callback_timeout_seconds": 10,
|
||||
"callback_max_retries": 2,
|
||||
"callback_retry_delay_seconds": 1.0,
|
||||
"callback_retry_backoff": 2.0,
|
||||
"callback_signing_secret": os.getenv("CALLBACK_SIGNING_SECRET")
|
||||
or os.getenv("CAPTCHA_CALLBACK_SIGNING_SECRET"),
|
||||
"balance": 999999.0,
|
||||
"task_cost": 0.0,
|
||||
"client_key": os.getenv("CLIENT_KEY") or os.getenv("CAPTCHA_CLIENT_KEY"),
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
@@ -281,6 +324,7 @@ SOLVER_TRAIN_CONFIG = {
|
||||
}
|
||||
|
||||
SOLVER_REGRESSION_RANGE = {
|
||||
"slide": (0, 1), # 归一化百分比
|
||||
# 在 solver 输入宽度空间内预测缺口中心 x 坐标,再通过 sigmoid 归一化到 [0, 1]
|
||||
"slide": (0, SOLVER_CONFIG["slide"]["cnn_input_size"][1]),
|
||||
"rotate": (0, 360), # 角度
|
||||
}
|
||||
|
||||
1
data/real/funcaptcha/4_3d_rollball_animals/.gitkeep
Normal file
1
data/real/funcaptcha/4_3d_rollball_animals/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
生成滑块验证码训练数据:随机纹理/色块背景 + 方形缺口 + 阴影效果。
|
||||
标签 = 缺口中心 x 坐标 (整数)
|
||||
文件名格式: {gap_x}_{index:06d}.png
|
||||
文件名格式: {gap_center_x}_{index:06d}.png
|
||||
"""
|
||||
|
||||
import random
|
||||
@@ -29,13 +29,14 @@ class SlideDataGenerator(BaseCaptchaGenerator):
|
||||
rng = self.rng
|
||||
gs = self.gap_size
|
||||
|
||||
# 缺口 x 范围: 留出边距
|
||||
# 缺口左边界 x 范围: 留出边距,标签统一使用缺口中心 x
|
||||
margin = gs + 10
|
||||
gap_x = rng.randint(margin, self.width - margin)
|
||||
gap_left = rng.randint(margin, self.width - margin)
|
||||
gap_y = rng.randint(10, self.height - gs - 10)
|
||||
gap_center_x = gap_left + gs // 2
|
||||
|
||||
if text is None:
|
||||
text = str(gap_x)
|
||||
text = str(gap_center_x)
|
||||
|
||||
# 1. 生成纹理背景
|
||||
img = self._textured_background(rng)
|
||||
@@ -46,12 +47,12 @@ class SlideDataGenerator(BaseCaptchaGenerator):
|
||||
|
||||
# 阴影 (稍大一圈)
|
||||
overlay_draw.rectangle(
|
||||
[gap_x + 2, gap_y + 2, gap_x + gs + 2, gap_y + gs + 2],
|
||||
[gap_left + 2, gap_y + 2, gap_left + gs + 2, gap_y + gs + 2],
|
||||
fill=(0, 0, 0, 60),
|
||||
)
|
||||
# 缺口本体
|
||||
overlay_draw.rectangle(
|
||||
[gap_x, gap_y, gap_x + gs, gap_y + gs],
|
||||
[gap_left, gap_y, gap_left + gs, gap_y + gs],
|
||||
fill=(80, 80, 80, 160),
|
||||
outline=(60, 60, 60, 200),
|
||||
width=2,
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
推理包
|
||||
|
||||
- pipeline.py: CaptchaPipeline 核心推理流水线
|
||||
- fun_captcha.py: FunCaptcha 专项推理
|
||||
- export_onnx.py: PyTorch → ONNX 导出
|
||||
- math_eval.py: 算式计算模块
|
||||
"""
|
||||
|
||||
from inference.pipeline import CaptchaPipeline
|
||||
from inference.fun_captcha import FunCaptchaRollballPipeline
|
||||
from inference.math_eval import eval_captcha_math
|
||||
from inference.export_onnx import export_model, export_all
|
||||
|
||||
__all__ = [
|
||||
"CaptchaPipeline",
|
||||
"FunCaptchaRollballPipeline",
|
||||
"eval_captcha_math",
|
||||
"export_model",
|
||||
"export_all",
|
||||
|
||||
@@ -9,7 +9,9 @@ import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from config import (
|
||||
CAPTCHA_TYPES,
|
||||
CHECKPOINTS_DIR,
|
||||
FUN_CAPTCHA_TASKS,
|
||||
ONNX_DIR,
|
||||
ONNX_CONFIG,
|
||||
IMAGE_SIZE,
|
||||
@@ -19,20 +21,28 @@ from config import (
|
||||
NUM_CAPTCHA_TYPES,
|
||||
REGRESSION_RANGE,
|
||||
SOLVER_CONFIG,
|
||||
SOLVER_REGRESSION_RANGE,
|
||||
)
|
||||
from inference.model_metadata import write_model_metadata
|
||||
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
|
||||
from models.fun_captcha_siamese import FunCaptchaSiamese
|
||||
|
||||
|
||||
def export_model(
|
||||
model: nn.Module,
|
||||
model_name: str,
|
||||
input_shape: tuple,
|
||||
input_shape: tuple | None = None,
|
||||
onnx_dir: str | None = None,
|
||||
metadata: dict | None = None,
|
||||
dummy_inputs: tuple[torch.Tensor, ...] | None = None,
|
||||
input_names: list[str] | None = None,
|
||||
output_names: list[str] | None = None,
|
||||
dynamic_axes: dict | None = None,
|
||||
):
|
||||
"""
|
||||
导出单个模型为 ONNX。
|
||||
@@ -52,25 +62,41 @@ def export_model(
|
||||
model.eval()
|
||||
model.cpu()
|
||||
|
||||
dummy = torch.randn(1, *input_shape)
|
||||
if dummy_inputs is None:
|
||||
if input_shape is None:
|
||||
raise ValueError("input_shape 和 dummy_inputs 不能同时为空")
|
||||
dummy_inputs = (torch.randn(1, *input_shape),)
|
||||
if input_names is None:
|
||||
input_names = ["input"] if len(dummy_inputs) == 1 else [f"input_{i}" for i in range(len(dummy_inputs))]
|
||||
if output_names is None:
|
||||
output_names = ["output"]
|
||||
|
||||
# 分类器和识别器的 dynamic_axes 不同
|
||||
if model_name == "classifier" or model_name in ("threed_rotate", "threed_slider", "gap_detector", "rotation_regressor"):
|
||||
dynamic_axes = {"input": {0: "batch"}, "output": {0: "batch"}}
|
||||
else:
|
||||
# CTC 模型: output shape = (T, B, C)
|
||||
dynamic_axes = {"input": {0: "batch"}, "output": {1: "batch"}}
|
||||
if dynamic_axes is None:
|
||||
if len(dummy_inputs) > 1:
|
||||
dynamic_axes = {name: {0: "batch"} for name in input_names}
|
||||
dynamic_axes.update({name: {0: "batch"} for name in output_names})
|
||||
elif model_name == "classifier" or model_name in (
|
||||
"threed_rotate", "threed_slider", "gap_detector", "rotation_regressor",
|
||||
"funcaptcha_rollball_animals",
|
||||
):
|
||||
dynamic_axes = {"input": {0: "batch"}, "output": {0: "batch"}}
|
||||
else:
|
||||
# CTC 模型: output shape = (T, B, C)
|
||||
dynamic_axes = {"input": {0: "batch"}, "output": {1: "batch"}}
|
||||
|
||||
torch.onnx.export(
|
||||
model,
|
||||
dummy,
|
||||
dummy_inputs[0] if len(dummy_inputs) == 1 else dummy_inputs,
|
||||
str(onnx_path),
|
||||
opset_version=ONNX_CONFIG["opset_version"],
|
||||
input_names=["input"],
|
||||
output_names=["output"],
|
||||
input_names=input_names,
|
||||
output_names=output_names,
|
||||
dynamic_axes=dynamic_axes if ONNX_CONFIG["dynamic_batch"] else None,
|
||||
)
|
||||
|
||||
if metadata is not None:
|
||||
write_model_metadata(onnx_path, metadata)
|
||||
|
||||
size_kb = onnx_path.stat().st_size / 1024
|
||||
print(f"[ONNX] 导出完成: {onnx_path} ({size_kb:.1f} KB)")
|
||||
|
||||
@@ -86,47 +112,126 @@ def _load_and_export(model_name: str):
|
||||
acc_info = ckpt.get('best_acc') or ckpt.get('best_tol_acc', '?')
|
||||
print(f"[加载] {model_name}: epoch={ckpt.get('epoch', '?')} acc={acc_info}")
|
||||
|
||||
metadata = None
|
||||
|
||||
if model_name == "classifier":
|
||||
model = CaptchaClassifier(num_types=NUM_CAPTCHA_TYPES)
|
||||
h, w = IMAGE_SIZE["classifier"]
|
||||
input_shape = (1, h, w)
|
||||
metadata = {
|
||||
"model_name": model_name,
|
||||
"task": "classifier",
|
||||
"class_names": list(ckpt.get("class_names", CAPTCHA_TYPES)),
|
||||
"input_shape": [1, h, w],
|
||||
}
|
||||
elif model_name == "normal":
|
||||
chars = ckpt.get("chars", NORMAL_CHARS)
|
||||
h, w = IMAGE_SIZE["normal"]
|
||||
model = LiteCRNN(chars=chars, img_h=h, img_w=w)
|
||||
input_shape = (1, h, w)
|
||||
metadata = {
|
||||
"model_name": model_name,
|
||||
"task": "ctc",
|
||||
"chars": chars,
|
||||
"input_shape": [1, h, w],
|
||||
}
|
||||
elif model_name == "math":
|
||||
chars = ckpt.get("chars", MATH_CHARS)
|
||||
h, w = IMAGE_SIZE["math"]
|
||||
model = LiteCRNN(chars=chars, img_h=h, img_w=w)
|
||||
input_shape = (1, h, w)
|
||||
metadata = {
|
||||
"model_name": model_name,
|
||||
"task": "ctc",
|
||||
"chars": chars,
|
||||
"input_shape": [1, h, w],
|
||||
}
|
||||
elif model_name == "threed_text":
|
||||
chars = ckpt.get("chars", THREED_CHARS)
|
||||
h, w = IMAGE_SIZE["3d_text"]
|
||||
model = ThreeDCNN(chars=chars, img_h=h, img_w=w)
|
||||
input_shape = (1, h, w)
|
||||
metadata = {
|
||||
"model_name": model_name,
|
||||
"task": "ctc",
|
||||
"chars": chars,
|
||||
"input_shape": [1, h, w],
|
||||
}
|
||||
elif model_name == "threed_rotate":
|
||||
h, w = IMAGE_SIZE["3d_rotate"]
|
||||
model = RegressionCNN(img_h=h, img_w=w)
|
||||
input_shape = (1, h, w)
|
||||
metadata = {
|
||||
"model_name": model_name,
|
||||
"task": "regression",
|
||||
"label_range": list(ckpt.get("label_range", REGRESSION_RANGE["3d_rotate"])),
|
||||
"input_shape": [1, h, w],
|
||||
}
|
||||
elif model_name == "threed_slider":
|
||||
h, w = IMAGE_SIZE["3d_slider"]
|
||||
model = RegressionCNN(img_h=h, img_w=w)
|
||||
input_shape = (1, h, w)
|
||||
metadata = {
|
||||
"model_name": model_name,
|
||||
"task": "regression",
|
||||
"label_range": list(ckpt.get("label_range", REGRESSION_RANGE["3d_slider"])),
|
||||
"input_shape": [1, h, w],
|
||||
}
|
||||
elif model_name == "gap_detector":
|
||||
h, w = SOLVER_CONFIG["slide"]["cnn_input_size"]
|
||||
model = GapDetectorCNN(img_h=h, img_w=w)
|
||||
input_shape = (1, h, w)
|
||||
metadata = {
|
||||
"model_name": model_name,
|
||||
"task": "regression",
|
||||
"label_range": list(ckpt.get("label_range", SOLVER_REGRESSION_RANGE["slide"])),
|
||||
"input_shape": [1, h, w],
|
||||
}
|
||||
elif model_name == "rotation_regressor":
|
||||
h, w = SOLVER_CONFIG["rotate"]["input_size"]
|
||||
model = RotationRegressor(img_h=h, img_w=w)
|
||||
input_shape = (3, h, w)
|
||||
metadata = {
|
||||
"model_name": model_name,
|
||||
"task": "rotation_solver",
|
||||
"output_encoding": "sin_cos",
|
||||
"input_shape": [3, h, w],
|
||||
}
|
||||
elif model_name == "funcaptcha_rollball_animals":
|
||||
question = "4_3d_rollball_animals"
|
||||
task_cfg = FUN_CAPTCHA_TASKS[question]
|
||||
h, w = task_cfg["input_size"]
|
||||
model = FunCaptchaSiamese(in_channels=task_cfg["channels"])
|
||||
metadata = {
|
||||
"model_name": model_name,
|
||||
"task": "funcaptcha_siamese",
|
||||
"question": question,
|
||||
"num_candidates": int(ckpt.get("num_candidates", task_cfg["num_candidates"])),
|
||||
"tile_size": list(ckpt.get("tile_size", task_cfg["tile_size"])),
|
||||
"reference_box": list(ckpt.get("reference_box", task_cfg["reference_box"])),
|
||||
"answer_index_base": int(ckpt.get("answer_index_base", task_cfg["answer_index_base"])),
|
||||
"input_shape": list(ckpt.get("input_shape", [task_cfg["channels"], h, w])),
|
||||
}
|
||||
else:
|
||||
print(f"[错误] 未知模型: {model_name}")
|
||||
return
|
||||
|
||||
model.load_state_dict(ckpt["model_state_dict"])
|
||||
export_model(model, model_name, input_shape)
|
||||
if model_name == "funcaptcha_rollball_animals":
|
||||
channels, h, w = metadata["input_shape"]
|
||||
export_model(
|
||||
model,
|
||||
model_name,
|
||||
metadata=metadata,
|
||||
dummy_inputs=(
|
||||
torch.randn(1, channels, h, w),
|
||||
torch.randn(1, channels, h, w),
|
||||
),
|
||||
input_names=["candidate", "reference"],
|
||||
output_names=["output"],
|
||||
)
|
||||
else:
|
||||
export_model(model, model_name, input_shape, metadata=metadata)
|
||||
|
||||
|
||||
def export_all():
|
||||
@@ -138,6 +243,7 @@ def export_all():
|
||||
"classifier", "normal", "math", "threed_text",
|
||||
"threed_rotate", "threed_slider",
|
||||
"gap_detector", "rotation_regressor",
|
||||
"funcaptcha_rollball_animals",
|
||||
]:
|
||||
_load_and_export(name)
|
||||
print("\n全部导出完成。")
|
||||
|
||||
117
inference/fun_captcha.py
Normal file
117
inference/fun_captcha.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
FunCaptcha 专项 ONNX 推理。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from config import FUN_CAPTCHA_TASKS, INFERENCE_CONFIG
|
||||
from inference.model_metadata import load_model_metadata
|
||||
from inference.pipeline import _try_import_ort
|
||||
|
||||
|
||||
class FunCaptchaRollballPipeline:
|
||||
"""
|
||||
`4_3d_rollball_animals` 专项推理器。
|
||||
|
||||
输入整张 challenge 图片,内部自动裁切 reference / candidates,
|
||||
再使用 Siamese ONNX 模型逐个候选打分。
|
||||
"""
|
||||
|
||||
def __init__(self, question: str = "4_3d_rollball_animals", models_dir: str | None = None):
|
||||
if question not in FUN_CAPTCHA_TASKS:
|
||||
raise ValueError(f"不支持的 FunCaptcha question: {question}")
|
||||
|
||||
ort = _try_import_ort()
|
||||
self.question = question
|
||||
self.task_cfg = FUN_CAPTCHA_TASKS[question]
|
||||
self.models_dir = Path(models_dir or INFERENCE_CONFIG["default_models_dir"])
|
||||
self.model_path = self.models_dir / f"{self.task_cfg['artifact_name']}.onnx"
|
||||
if not self.model_path.exists():
|
||||
raise FileNotFoundError(f"未找到 FunCaptcha ONNX 模型: {self.model_path}")
|
||||
|
||||
opts = ort.SessionOptions()
|
||||
opts.inter_op_num_threads = 1
|
||||
opts.intra_op_num_threads = 2
|
||||
self.session = ort.InferenceSession(
|
||||
str(self.model_path),
|
||||
sess_options=opts,
|
||||
providers=["CPUExecutionProvider"],
|
||||
)
|
||||
self.metadata = load_model_metadata(self.model_path) or {}
|
||||
self.mean = float(INFERENCE_CONFIG["normalize_mean"])
|
||||
self.std = float(INFERENCE_CONFIG["normalize_std"])
|
||||
self.answer_index_base = int(
|
||||
self.metadata.get("answer_index_base", self.task_cfg["answer_index_base"])
|
||||
)
|
||||
|
||||
def solve(self, image) -> dict:
|
||||
t0 = time.perf_counter()
|
||||
challenge = self._load_image(image)
|
||||
candidates, reference = self._split_challenge(challenge)
|
||||
|
||||
ref_batch = np.repeat(reference, repeats=candidates.shape[0], axis=0)
|
||||
input_names = [inp.name for inp in self.session.get_inputs()]
|
||||
if len(input_names) != 2:
|
||||
raise RuntimeError(f"专项模型输入数量异常: expected=2 got={len(input_names)}")
|
||||
|
||||
logits = self.session.run(None, {
|
||||
input_names[0]: candidates,
|
||||
input_names[1]: ref_batch,
|
||||
})[0].reshape(-1)
|
||||
scores = 1.0 / (1.0 + np.exp(-logits))
|
||||
answer_idx = int(np.argmax(logits))
|
||||
selected = answer_idx + self.answer_index_base
|
||||
elapsed = (time.perf_counter() - t0) * 1000
|
||||
|
||||
return {
|
||||
"type": "funcaptcha",
|
||||
"question": self.question,
|
||||
"objects": [selected],
|
||||
"scores": [round(float(score), 6) for score in scores.tolist()],
|
||||
"raw": str(selected),
|
||||
"result": str(selected),
|
||||
"time_ms": round(elapsed, 2),
|
||||
}
|
||||
|
||||
def _split_challenge(self, image: Image.Image) -> tuple[np.ndarray, np.ndarray]:
|
||||
tile_w, tile_h = self.metadata.get("tile_size", self.task_cfg["tile_size"])
|
||||
ref_box = tuple(self.metadata.get("reference_box", self.task_cfg["reference_box"]))
|
||||
num_candidates = int(self.metadata.get("num_candidates", self.task_cfg["num_candidates"]))
|
||||
input_h, input_w = self.task_cfg["input_size"]
|
||||
|
||||
candidates = []
|
||||
for idx in range(num_candidates):
|
||||
left = idx * tile_w
|
||||
candidate = image.crop((left, 0, left + tile_w, tile_h))
|
||||
candidates.append(self._preprocess(candidate, (input_h, input_w)))
|
||||
|
||||
reference = image.crop(ref_box)
|
||||
return (
|
||||
np.concatenate(candidates, axis=0),
|
||||
self._preprocess(reference, (input_h, input_w)),
|
||||
)
|
||||
|
||||
def _preprocess(self, image: Image.Image, target_size: tuple[int, int]) -> np.ndarray:
|
||||
img_h, img_w = target_size
|
||||
image = image.convert("RGB").resize((img_w, img_h), Image.BILINEAR)
|
||||
arr = np.asarray(image, dtype=np.float32) / 255.0
|
||||
arr = (arr - self.mean) / self.std
|
||||
arr = np.transpose(arr, (2, 0, 1))
|
||||
return arr.reshape(1, 3, img_h, img_w)
|
||||
|
||||
@staticmethod
|
||||
def _load_image(image) -> Image.Image:
|
||||
if isinstance(image, Image.Image):
|
||||
return image
|
||||
if isinstance(image, (str, Path)):
|
||||
return Image.open(image).convert("RGB")
|
||||
if isinstance(image, bytes):
|
||||
return Image.open(io.BytesIO(image)).convert("RGB")
|
||||
raise TypeError(f"不支持的图片输入类型: {type(image)}")
|
||||
33
inference/model_metadata.py
Normal file
33
inference/model_metadata.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
ONNX 模型 sidecar metadata 辅助工具。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def model_metadata_path(model_path: str | Path) -> Path:
|
||||
return Path(model_path).with_suffix(".meta.json")
|
||||
|
||||
|
||||
def write_model_metadata(model_path: str | Path, metadata: dict) -> Path:
|
||||
path = model_metadata_path(model_path)
|
||||
payload = {
|
||||
"version": 1,
|
||||
**metadata,
|
||||
}
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as f:
|
||||
json.dump(payload, f, ensure_ascii=True, indent=2, sort_keys=True)
|
||||
f.write("\n")
|
||||
return path
|
||||
|
||||
|
||||
def load_model_metadata(model_path: str | Path) -> dict | None:
|
||||
path = model_metadata_path(model_path)
|
||||
if not path.exists():
|
||||
return None
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
@@ -26,6 +26,7 @@ from config import (
|
||||
REGRESSION_RANGE,
|
||||
)
|
||||
from inference.math_eval import eval_captcha_math
|
||||
from inference.model_metadata import load_model_metadata
|
||||
|
||||
|
||||
def _try_import_ort():
|
||||
@@ -66,6 +67,11 @@ class CaptchaPipeline:
|
||||
"math": MATH_CHARS,
|
||||
"3d_text": THREED_CHARS,
|
||||
}
|
||||
self._classifier_class_names = tuple(CAPTCHA_TYPES)
|
||||
self._regression_ranges = {
|
||||
"3d_rotate": REGRESSION_RANGE["3d_rotate"],
|
||||
"3d_slider": REGRESSION_RANGE["3d_slider"],
|
||||
}
|
||||
|
||||
# 回归模型类型
|
||||
self._regression_types = {"3d_rotate", "3d_slider"}
|
||||
@@ -86,6 +92,7 @@ class CaptchaPipeline:
|
||||
opts.intra_op_num_threads = 2
|
||||
|
||||
self._sessions: dict[str, "ort.InferenceSession"] = {}
|
||||
self._metadata: dict[str, dict] = {}
|
||||
for name, fname in self._model_files.items():
|
||||
path = self.models_dir / fname
|
||||
if path.exists():
|
||||
@@ -93,6 +100,7 @@ class CaptchaPipeline:
|
||||
str(path), sess_options=opts,
|
||||
providers=["CPUExecutionProvider"],
|
||||
)
|
||||
self._metadata[name] = load_model_metadata(path) or {}
|
||||
|
||||
loaded = list(self._sessions.keys())
|
||||
if not loaded:
|
||||
@@ -135,7 +143,14 @@ class CaptchaPipeline:
|
||||
input_name = session.get_inputs()[0].name
|
||||
logits = session.run(None, {input_name: inp})[0] # (1, num_types)
|
||||
idx = int(np.argmax(logits, axis=1)[0])
|
||||
return CAPTCHA_TYPES[idx]
|
||||
class_names = tuple(
|
||||
self._metadata.get("classifier", {}).get("class_names", self._classifier_class_names)
|
||||
)
|
||||
if idx >= len(class_names):
|
||||
raise RuntimeError(
|
||||
f"分类器输出索引越界: idx={idx}, classes={len(class_names)}"
|
||||
)
|
||||
return class_names[idx]
|
||||
|
||||
def solve(
|
||||
self,
|
||||
@@ -182,14 +197,17 @@ class CaptchaPipeline:
|
||||
# 回归模型: 输出 (batch, 1) sigmoid 值
|
||||
output = session.run(None, {input_name: inp})[0] # (1, 1)
|
||||
sigmoid_val = float(output[0, 0])
|
||||
lo, hi = REGRESSION_RANGE[captcha_type]
|
||||
lo, hi = self._metadata.get(captcha_type, {}).get(
|
||||
"label_range",
|
||||
self._regression_ranges[captcha_type],
|
||||
)
|
||||
real_val = sigmoid_val * (hi - lo) + lo
|
||||
raw_text = f"{real_val:.1f}"
|
||||
result = str(int(round(real_val)))
|
||||
else:
|
||||
# CTC 模型
|
||||
logits = session.run(None, {input_name: inp})[0] # (T, 1, C)
|
||||
chars = self._chars[captcha_type]
|
||||
chars = self._metadata.get(captcha_type, {}).get("chars", self._chars[captcha_type])
|
||||
raw_text = self._ctc_greedy_decode(logits, chars)
|
||||
|
||||
# 后处理
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
- RegressionCNN: 回归 CNN (3D 旋转 + 滑块, ~1MB)
|
||||
- GapDetectorCNN: 滑块缺口检测 CNN (~1MB)
|
||||
- RotationRegressor: 旋转角度回归 sin/cos 编码 (~2MB)
|
||||
- FunCaptchaSiamese: FunCaptcha 专项 Siamese 匹配模型
|
||||
"""
|
||||
|
||||
from models.classifier import CaptchaClassifier
|
||||
@@ -16,6 +17,7 @@ from models.threed_cnn import ThreeDCNN
|
||||
from models.regression_cnn import RegressionCNN
|
||||
from models.gap_detector import GapDetectorCNN
|
||||
from models.rotation_regressor import RotationRegressor
|
||||
from models.fun_captcha_siamese import FunCaptchaSiamese
|
||||
|
||||
__all__ = [
|
||||
"CaptchaClassifier",
|
||||
@@ -24,4 +26,5 @@ __all__ = [
|
||||
"RegressionCNN",
|
||||
"GapDetectorCNN",
|
||||
"RotationRegressor",
|
||||
"FunCaptchaSiamese",
|
||||
]
|
||||
|
||||
@@ -22,7 +22,7 @@ class CaptchaClassifier(nn.Module):
|
||||
→ 全局平均池化 → 全连接 → 输出类别数。
|
||||
"""
|
||||
|
||||
def __init__(self, num_types: int = 3):
|
||||
def __init__(self, num_types: int = 5):
|
||||
super().__init__()
|
||||
self.num_types = num_types
|
||||
|
||||
|
||||
72
models/fun_captcha_siamese.py
Normal file
72
models/fun_captcha_siamese.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
FunCaptcha 专项 Siamese 模型。
|
||||
|
||||
用于 `4_3d_rollball_animals` 这类 challenge:
|
||||
- 输入 1: 候选块 candidate (RGB)
|
||||
- 输入 2: 参考块 reference (RGB)
|
||||
- 输出: 单个匹配 logit,值越大表示越可能为正确候选
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
class _SharedEncoder(nn.Module):
|
||||
def __init__(self, in_channels: int = 3, embedding_dim: int = 128):
|
||||
super().__init__()
|
||||
self.features = nn.Sequential(
|
||||
nn.Conv2d(in_channels, 32, kernel_size=3, padding=1),
|
||||
nn.BatchNorm2d(32),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.MaxPool2d(2, 2),
|
||||
|
||||
nn.Conv2d(32, 64, kernel_size=3, padding=1),
|
||||
nn.BatchNorm2d(64),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.MaxPool2d(2, 2),
|
||||
|
||||
nn.Conv2d(64, 96, kernel_size=3, padding=1),
|
||||
nn.BatchNorm2d(96),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.MaxPool2d(2, 2),
|
||||
|
||||
nn.Conv2d(96, 128, kernel_size=3, padding=1),
|
||||
nn.BatchNorm2d(128),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.AdaptiveAvgPool2d(1),
|
||||
)
|
||||
self.proj = nn.Linear(128, embedding_dim)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
x = self.features(x)
|
||||
x = x.view(x.size(0), -1)
|
||||
return self.proj(x)
|
||||
|
||||
|
||||
class FunCaptchaSiamese(nn.Module):
|
||||
"""
|
||||
共享编码器 + 特征对比头。
|
||||
|
||||
输出 raw logits,训练时配合 `BCEWithLogitsLoss` 使用。
|
||||
"""
|
||||
|
||||
def __init__(self, in_channels: int = 3, embedding_dim: int = 128):
|
||||
super().__init__()
|
||||
self.encoder = _SharedEncoder(in_channels=in_channels, embedding_dim=embedding_dim)
|
||||
self.head = nn.Sequential(
|
||||
nn.Linear(embedding_dim * 4, 128),
|
||||
nn.ReLU(inplace=True),
|
||||
nn.Dropout(p=0.1),
|
||||
nn.Linear(128, 1),
|
||||
)
|
||||
|
||||
def forward(self, candidate: torch.Tensor, reference: torch.Tensor) -> torch.Tensor:
|
||||
candidate_feat = self.encoder(candidate)
|
||||
reference_feat = self.encoder(reference)
|
||||
|
||||
diff = torch.abs(candidate_feat - reference_feat)
|
||||
prod = candidate_feat * reference_feat
|
||||
features = torch.cat([candidate_feat, reference_feat, diff, prod], dim=1)
|
||||
return self.head(features)
|
||||
@@ -1,14 +1,18 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "captchbreaker"
|
||||
version = "0.1.0"
|
||||
description = "验证码识别多模型系统 - 调度模型 + 多专家模型两级架构"
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
dependencies = [
|
||||
"torch>=2.0.0",
|
||||
"torchvision>=0.15.0",
|
||||
"torch==2.5.1",
|
||||
"torchvision==0.20.1",
|
||||
"onnx>=1.14.0",
|
||||
"onnxscript>=0.6.0",
|
||||
"onnxruntime>=1.15.0",
|
||||
"onnxruntime>=1.15.0,<1.24.0",
|
||||
"pillow>=10.0.0",
|
||||
"numpy>=1.24.0",
|
||||
"tqdm>=4.65.0",
|
||||
@@ -29,3 +33,35 @@ dev = [
|
||||
|
||||
[project.scripts]
|
||||
captcha = "cli:main"
|
||||
|
||||
[tool.uv.sources]
|
||||
torch = [
|
||||
{ index = "pytorch-cu121", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
|
||||
]
|
||||
torchvision = [
|
||||
{ index = "pytorch-cu121", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "pytorch-cu121"
|
||||
url = "https://download.pytorch.org/whl/cu121"
|
||||
explicit = true
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = ["cli", "config", "server"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = [
|
||||
"generators",
|
||||
"generators.*",
|
||||
"models",
|
||||
"models.*",
|
||||
"training",
|
||||
"training.*",
|
||||
"inference",
|
||||
"inference.*",
|
||||
"solvers",
|
||||
"solvers.*",
|
||||
"utils",
|
||||
"utils.*",
|
||||
]
|
||||
|
||||
816
server.py
816
server.py
@@ -4,117 +4,819 @@ FastAPI HTTP 推理服务 (纯推理,不依赖 torch/训练代码)
|
||||
仅依赖: fastapi, uvicorn, python-multipart, onnxruntime, pillow, numpy
|
||||
|
||||
API:
|
||||
POST /solve JSON base64 图片识别
|
||||
POST /solve/upload multipart 文件上传识别
|
||||
GET /health 健康检查
|
||||
POST /solve JSON base64 图片识别 (同步)
|
||||
POST /solve/upload multipart 文件上传识别 (同步)
|
||||
POST /createTask 创建异步识别任务
|
||||
POST /getTaskResult 查询异步任务结果
|
||||
POST /getBalance 查询本地服务余额占位值
|
||||
POST /api/v1/* 兼容别名
|
||||
GET /health 健康检查
|
||||
GET /api/v1/health 健康检查兼容别名
|
||||
|
||||
启动:
|
||||
uv sync --extra server
|
||||
python cli.py serve --port 8080
|
||||
|
||||
请求示例 (base64):
|
||||
curl -X POST http://localhost:8080/solve \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"image": "<base64>", "type": "normal"}'
|
||||
|
||||
请求示例 (文件上传):
|
||||
curl -X POST http://localhost:8080/solve/upload -F "image=@captcha.png"
|
||||
|
||||
响应:
|
||||
{"type": "normal", "result": "A3B8", "raw": "A3B8", "time_ms": 12.3}
|
||||
uv run captcha serve --port 8080
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from urllib.error import URLError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request as UrlRequest, urlopen
|
||||
|
||||
from config import CAPTCHA_TYPES, FUN_CAPTCHA_TASKS, SERVER_CONFIG
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_app():
|
||||
ASYNC_TASK_TYPES = {
|
||||
"ImageToTextTask",
|
||||
"ImageToTextTaskM1",
|
||||
"ImageToTextTaskMuggle",
|
||||
"ImageToTextTaskProxyless",
|
||||
"CaptchaImageTask",
|
||||
"FunCaptcha",
|
||||
"FunCaptchaTask",
|
||||
"FunCaptchaTaskProxyless",
|
||||
}
|
||||
|
||||
|
||||
class ServiceError(Exception):
|
||||
"""服务内部可预期错误。"""
|
||||
|
||||
def __init__(self, code: str, description: str, status_code: int = 400):
|
||||
super().__init__(description)
|
||||
self.code = code
|
||||
self.description = description
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskRecord:
|
||||
task_id: str
|
||||
created_at: int
|
||||
expires_at: int
|
||||
client_ip: str | None = None
|
||||
task_type: str | None = None
|
||||
captcha_type: str | None = None
|
||||
question: str | None = None
|
||||
callback_url: str | None = None
|
||||
callback_attempts: int = 0
|
||||
callback_delivered_at: int | None = None
|
||||
callback_last_error: str | None = None
|
||||
status: str = "processing"
|
||||
result: dict | None = None
|
||||
error_code: str | None = None
|
||||
error_description: str | None = None
|
||||
completed_at: int | None = None
|
||||
|
||||
|
||||
class TaskManager:
|
||||
"""简单的内存任务管理器,适合单进程部署。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
solve_fn: Callable[[bytes, str | None, str | None], dict],
|
||||
ttl_seconds: int,
|
||||
max_workers: int,
|
||||
tasks_dir: str | Path,
|
||||
):
|
||||
self._solve_fn = solve_fn
|
||||
self._ttl_seconds = ttl_seconds
|
||||
self._tasks_dir = Path(tasks_dir)
|
||||
self._tasks_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._executor = ThreadPoolExecutor(
|
||||
max_workers=max_workers,
|
||||
thread_name_prefix="captcha-task",
|
||||
)
|
||||
self._tasks: dict[str, TaskRecord] = {}
|
||||
self._lock = threading.Lock()
|
||||
self._load_tasks()
|
||||
|
||||
def create_task(
|
||||
self,
|
||||
image_bytes: bytes,
|
||||
captcha_type: str | None,
|
||||
question: str | None = None,
|
||||
client_ip: str | None = None,
|
||||
task_type: str | None = None,
|
||||
callback_url: str | None = None,
|
||||
) -> str:
|
||||
now = int(time.time())
|
||||
task_id = uuid.uuid4().hex
|
||||
record = TaskRecord(
|
||||
task_id=task_id,
|
||||
created_at=now,
|
||||
expires_at=now + self._ttl_seconds,
|
||||
client_ip=client_ip,
|
||||
task_type=task_type,
|
||||
captcha_type=captcha_type,
|
||||
question=question,
|
||||
callback_url=callback_url,
|
||||
)
|
||||
with self._lock:
|
||||
self._cleanup_locked(now)
|
||||
self._tasks[task_id] = record
|
||||
self._persist_task_locked(record)
|
||||
|
||||
self._executor.submit(self._run_task, task_id, image_bytes, captcha_type, question)
|
||||
return task_id
|
||||
|
||||
def get_task(self, task_id: str) -> TaskRecord | None:
|
||||
with self._lock:
|
||||
self._cleanup_locked()
|
||||
return self._tasks.get(task_id)
|
||||
|
||||
def stats(self) -> dict:
|
||||
with self._lock:
|
||||
self._cleanup_locked()
|
||||
processing = sum(1 for task in self._tasks.values() if task.status == "processing")
|
||||
ready = sum(1 for task in self._tasks.values() if task.status == "ready")
|
||||
failed = sum(1 for task in self._tasks.values() if task.status == "failed")
|
||||
return {
|
||||
"active": len(self._tasks),
|
||||
"processing": processing,
|
||||
"ready": ready,
|
||||
"failed": failed,
|
||||
"ttl_seconds": self._ttl_seconds,
|
||||
}
|
||||
|
||||
def shutdown(self):
|
||||
self._executor.shutdown(wait=True, cancel_futures=False)
|
||||
|
||||
def _run_task(
|
||||
self,
|
||||
task_id: str,
|
||||
image_bytes: bytes,
|
||||
captcha_type: str | None,
|
||||
question: str | None,
|
||||
):
|
||||
try:
|
||||
result = self._solve_fn(image_bytes, captcha_type, question)
|
||||
except ServiceError as exc:
|
||||
task = self._mark_failed(task_id, exc.code, exc.description)
|
||||
except Exception as exc: # pragma: no cover - 防御性兜底
|
||||
task = self._mark_failed(task_id, "ERROR_TASK_FAILED", str(exc))
|
||||
else:
|
||||
task = self._mark_ready(task_id, result)
|
||||
|
||||
if task and task.callback_url:
|
||||
self._send_task_callback(task)
|
||||
|
||||
def _mark_ready(self, task_id: str, result: dict) -> TaskRecord | None:
|
||||
with self._lock:
|
||||
record = self._tasks.get(task_id)
|
||||
if record is None:
|
||||
return None
|
||||
record.status = "ready"
|
||||
record.result = result
|
||||
record.completed_at = int(time.time())
|
||||
self._persist_task_locked(record)
|
||||
return record
|
||||
|
||||
def _mark_failed(self, task_id: str, code: str, description: str) -> TaskRecord | None:
|
||||
with self._lock:
|
||||
record = self._tasks.get(task_id)
|
||||
if record is None:
|
||||
return None
|
||||
record.status = "failed"
|
||||
record.error_code = code
|
||||
record.error_description = description
|
||||
record.completed_at = int(time.time())
|
||||
self._persist_task_locked(record)
|
||||
return record
|
||||
|
||||
def _cleanup_locked(self, now: int | None = None):
|
||||
now = now or int(time.time())
|
||||
expired_ids = [
|
||||
task_id
|
||||
for task_id, task in self._tasks.items()
|
||||
if task.expires_at <= now
|
||||
]
|
||||
for task_id in expired_ids:
|
||||
self._tasks.pop(task_id, None)
|
||||
self._delete_task_file(task_id)
|
||||
|
||||
def _update_callback_state(
|
||||
self,
|
||||
task_id: str,
|
||||
*,
|
||||
attempts: int | None = None,
|
||||
delivered_at: int | None = None,
|
||||
last_error: str | None = None,
|
||||
):
|
||||
with self._lock:
|
||||
record = self._tasks.get(task_id)
|
||||
if record is None:
|
||||
return
|
||||
if attempts is not None:
|
||||
record.callback_attempts = attempts
|
||||
if delivered_at is not None:
|
||||
record.callback_delivered_at = delivered_at
|
||||
record.callback_last_error = last_error
|
||||
self._persist_task_locked(record)
|
||||
|
||||
def _task_path(self, task_id: str) -> Path:
|
||||
return self._tasks_dir / f"{task_id}.json"
|
||||
|
||||
def _persist_task_locked(self, record: TaskRecord):
|
||||
path = self._task_path(record.task_id)
|
||||
tmp_path = path.with_suffix(".json.tmp")
|
||||
tmp_path.write_text(
|
||||
json.dumps(asdict(record), ensure_ascii=False, sort_keys=True),
|
||||
encoding="utf-8",
|
||||
)
|
||||
tmp_path.replace(path)
|
||||
|
||||
def _delete_task_file(self, task_id: str):
|
||||
self._task_path(task_id).unlink(missing_ok=True)
|
||||
|
||||
def _load_tasks(self):
|
||||
now = int(time.time())
|
||||
for path in sorted(self._tasks_dir.glob("*.json")):
|
||||
try:
|
||||
task_data = json.loads(path.read_text(encoding="utf-8"))
|
||||
task = TaskRecord(**task_data)
|
||||
except Exception as exc: # pragma: no cover - 防御性兜底
|
||||
logger.warning("skip invalid task file: path=%s err=%s", path, exc)
|
||||
path.unlink(missing_ok=True)
|
||||
continue
|
||||
|
||||
if task.expires_at <= now:
|
||||
path.unlink(missing_ok=True)
|
||||
continue
|
||||
|
||||
self._tasks[task.task_id] = task
|
||||
|
||||
def _send_task_callback(self, task: TaskRecord):
|
||||
max_retries = max(0, int(SERVER_CONFIG.get("callback_max_retries", 0)))
|
||||
delay = max(0.0, float(SERVER_CONFIG.get("callback_retry_delay_seconds", 0.0)))
|
||||
backoff = max(1.0, float(SERVER_CONFIG.get("callback_retry_backoff", 1.0)))
|
||||
payload = _build_task_callback_payload(task)
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
attempt_no = attempt + 1
|
||||
try:
|
||||
_post_callback(task.callback_url, payload)
|
||||
self._update_callback_state(
|
||||
task.task_id,
|
||||
attempts=attempt_no,
|
||||
delivered_at=int(time.time()),
|
||||
last_error=None,
|
||||
)
|
||||
return
|
||||
except (URLError, OSError, ValueError) as exc:
|
||||
self._update_callback_state(
|
||||
task.task_id,
|
||||
attempts=attempt_no,
|
||||
last_error=str(exc),
|
||||
)
|
||||
if attempt >= max_retries:
|
||||
logger.warning(
|
||||
"task callback failed: task_id=%s url=%s attempts=%s err=%s",
|
||||
task.task_id,
|
||||
task.callback_url,
|
||||
attempt_no,
|
||||
exc,
|
||||
)
|
||||
return
|
||||
|
||||
logger.warning(
|
||||
"task callback retry: task_id=%s url=%s attempt=%s err=%s",
|
||||
task.task_id,
|
||||
task.callback_url,
|
||||
attempt_no,
|
||||
exc,
|
||||
)
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
delay *= backoff
|
||||
|
||||
|
||||
def _task_success_payload(**extra) -> dict:
|
||||
payload = {"errorId": 0}
|
||||
payload.update(extra)
|
||||
return payload
|
||||
|
||||
|
||||
def _task_error_payload(code: str, description: str, **extra) -> dict:
|
||||
payload = {
|
||||
"errorId": 1,
|
||||
"errorCode": code,
|
||||
"errorDescription": description,
|
||||
}
|
||||
payload.update(extra)
|
||||
return payload
|
||||
|
||||
|
||||
def _normalize_captcha_type(captcha_type: str | None) -> str | None:
|
||||
if captcha_type in (None, ""):
|
||||
return None
|
||||
if captcha_type not in CAPTCHA_TYPES:
|
||||
raise ServiceError(
|
||||
"ERROR_BAD_CAPTCHA_TYPE",
|
||||
f"不支持的验证码类型: {captcha_type}",
|
||||
status_code=400,
|
||||
)
|
||||
return captcha_type
|
||||
|
||||
|
||||
def _normalize_question(question: str | None) -> str | None:
|
||||
if question in (None, ""):
|
||||
return None
|
||||
if question not in FUN_CAPTCHA_TASKS:
|
||||
raise ServiceError(
|
||||
"ERROR_TASK_QUESTION_NOT_SUPPORTED",
|
||||
f"不支持的专项任务 question: {question}",
|
||||
status_code=400,
|
||||
)
|
||||
return question
|
||||
|
||||
|
||||
def _decode_image_b64(encoded: str) -> bytes:
|
||||
if not encoded:
|
||||
raise ServiceError("ERROR_EMPTY_IMAGE", "空图片", status_code=400)
|
||||
|
||||
if encoded.startswith("data:") and "," in encoded:
|
||||
encoded = encoded.split(",", 1)[1]
|
||||
|
||||
encoded = "".join(encoded.split())
|
||||
encoded += "=" * (-len(encoded) % 4)
|
||||
|
||||
try:
|
||||
return base64.b64decode(encoded, altchars=b"-_", validate=True)
|
||||
except (binascii.Error, ValueError) as exc:
|
||||
raise ServiceError(
|
||||
"ERROR_BAD_IMAGE_BASE64",
|
||||
"base64 解码失败",
|
||||
status_code=400,
|
||||
) from exc
|
||||
|
||||
|
||||
def _validate_client_key(client_key: str | None):
|
||||
expected = SERVER_CONFIG.get("client_key")
|
||||
if not expected:
|
||||
return
|
||||
if client_key != expected:
|
||||
raise ServiceError(
|
||||
"ERROR_KEY_DOES_NOT_EXIST",
|
||||
"clientKey 无效",
|
||||
status_code=401,
|
||||
)
|
||||
|
||||
|
||||
def _build_task_solution(task: TaskRecord) -> dict:
|
||||
if task.result is None:
|
||||
return {}
|
||||
|
||||
if "objects" in task.result:
|
||||
objects = list(task.result["objects"])
|
||||
primary = objects[0] if objects else None
|
||||
solution = {
|
||||
"objects": objects,
|
||||
"answer": primary,
|
||||
"raw": task.result.get("raw", "" if primary is None else str(primary)),
|
||||
"timeMs": task.result["time_ms"],
|
||||
}
|
||||
if task.question:
|
||||
solution["question"] = task.question
|
||||
if primary is not None:
|
||||
solution["text"] = str(primary)
|
||||
return solution
|
||||
|
||||
return {
|
||||
"text": task.result["result"],
|
||||
"answer": task.result["result"],
|
||||
"raw": task.result["raw"],
|
||||
"captchaType": task.result["type"],
|
||||
"timeMs": task.result["time_ms"],
|
||||
}
|
||||
|
||||
|
||||
def _build_task_meta(task: TaskRecord) -> dict:
|
||||
payload = {
|
||||
"type": task.task_type,
|
||||
"captchaType": task.captcha_type,
|
||||
}
|
||||
if task.question is not None:
|
||||
payload["question"] = task.question
|
||||
return payload
|
||||
|
||||
|
||||
def _build_task_result_payload(task: TaskRecord) -> dict:
|
||||
if task.status == "processing":
|
||||
return _task_success_payload(
|
||||
taskId=task.task_id,
|
||||
status="processing",
|
||||
createTime=task.created_at,
|
||||
expiresAt=task.expires_at,
|
||||
task=_build_task_meta(task),
|
||||
callback={
|
||||
"configured": bool(task.callback_url),
|
||||
"url": task.callback_url,
|
||||
"attempts": task.callback_attempts,
|
||||
"delivered": False,
|
||||
"deliveredAt": task.callback_delivered_at,
|
||||
"lastError": task.callback_last_error,
|
||||
},
|
||||
)
|
||||
|
||||
if task.status == "failed":
|
||||
return _task_error_payload(
|
||||
task.error_code or "ERROR_TASK_FAILED",
|
||||
task.error_description or "任务执行失败",
|
||||
taskId=task.task_id,
|
||||
status="failed",
|
||||
ip=task.client_ip,
|
||||
createTime=task.created_at,
|
||||
endTime=task.completed_at,
|
||||
expiresAt=task.expires_at,
|
||||
task=_build_task_meta(task),
|
||||
callback={
|
||||
"configured": bool(task.callback_url),
|
||||
"url": task.callback_url,
|
||||
"attempts": task.callback_attempts,
|
||||
"delivered": task.callback_delivered_at is not None,
|
||||
"deliveredAt": task.callback_delivered_at,
|
||||
"lastError": task.callback_last_error,
|
||||
},
|
||||
)
|
||||
|
||||
return _task_success_payload(
|
||||
taskId=task.task_id,
|
||||
status="ready",
|
||||
solution=_build_task_solution(task),
|
||||
cost=f"{float(SERVER_CONFIG['task_cost']):.5f}",
|
||||
ip=task.client_ip,
|
||||
createTime=task.created_at,
|
||||
endTime=task.completed_at,
|
||||
expiresAt=task.expires_at,
|
||||
solveCount=1,
|
||||
task=_build_task_meta(task),
|
||||
callback={
|
||||
"configured": bool(task.callback_url),
|
||||
"url": task.callback_url,
|
||||
"attempts": task.callback_attempts,
|
||||
"delivered": task.callback_delivered_at is not None,
|
||||
"deliveredAt": task.callback_delivered_at,
|
||||
"lastError": task.callback_last_error,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _build_task_callback_payload(task: TaskRecord) -> dict[str, str]:
|
||||
payload = {
|
||||
"id": task.task_id,
|
||||
"taskId": task.task_id,
|
||||
"status": task.status,
|
||||
"errorId": "0" if task.status == "ready" else "1",
|
||||
}
|
||||
if task.status == "ready":
|
||||
solution = _build_task_solution(task)
|
||||
payload["code"] = str(solution.get("answer", ""))
|
||||
payload["answer"] = str(solution.get("answer", ""))
|
||||
payload["raw"] = str(solution.get("raw", ""))
|
||||
payload["timeMs"] = str(solution["timeMs"])
|
||||
payload["cost"] = f"{float(SERVER_CONFIG['task_cost']):.5f}"
|
||||
if "text" in solution:
|
||||
payload["text"] = str(solution["text"])
|
||||
if "captchaType" in solution:
|
||||
payload["captchaType"] = str(solution["captchaType"])
|
||||
if "objects" in solution:
|
||||
payload["objects"] = json.dumps(solution["objects"], ensure_ascii=False)
|
||||
if "question" in solution:
|
||||
payload["question"] = str(solution["question"])
|
||||
else:
|
||||
payload.update(
|
||||
{
|
||||
"errorCode": task.error_code or "ERROR_TASK_FAILED",
|
||||
"errorDescription": task.error_description or "任务执行失败",
|
||||
}
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def _sign_callback_payload(data: bytes, timestamp: str, secret: str) -> str:
|
||||
message = timestamp.encode("utf-8") + b"." + data
|
||||
return hmac.new(secret.encode("utf-8"), message, hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def _build_callback_request(callback_url: str, payload: dict[str, str]) -> UrlRequest:
|
||||
data = urlencode(payload).encode("utf-8")
|
||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
secret = SERVER_CONFIG.get("callback_signing_secret")
|
||||
if secret:
|
||||
timestamp = str(int(time.time()))
|
||||
headers["X-CaptchaBreaker-Timestamp"] = timestamp
|
||||
headers["X-CaptchaBreaker-Signature-Alg"] = "hmac-sha256"
|
||||
headers["X-CaptchaBreaker-Signature"] = _sign_callback_payload(data, timestamp, secret)
|
||||
|
||||
return UrlRequest(
|
||||
callback_url,
|
||||
data=data,
|
||||
headers=headers,
|
||||
method="POST",
|
||||
)
|
||||
|
||||
|
||||
def _post_callback(callback_url: str, payload: dict[str, str]):
|
||||
request = _build_callback_request(callback_url, payload)
|
||||
timeout = SERVER_CONFIG["callback_timeout_seconds"]
|
||||
with urlopen(request, timeout=timeout) as response:
|
||||
response.read()
|
||||
|
||||
|
||||
|
||||
def create_app(pipeline_factory=None, funcaptcha_factories=None):
|
||||
"""
|
||||
创建 FastAPI 应用实例(工厂函数)。
|
||||
|
||||
cli.py 的 cmd_serve 依赖此签名。
|
||||
"""
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI, File, Query, UploadFile
|
||||
from fastapi import Body, FastAPI, File, Query, Request, UploadFile
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from inference.fun_captcha import FunCaptchaRollballPipeline
|
||||
from inference.pipeline import CaptchaPipeline
|
||||
|
||||
app = FastAPI(
|
||||
title="CaptchaBreaker",
|
||||
description="验证码识别多模型系统 - HTTP 推理服务",
|
||||
version="0.1.0",
|
||||
)
|
||||
|
||||
pipeline: Optional[CaptchaPipeline] = None
|
||||
load_pipeline_on_startup = pipeline_factory is None
|
||||
pipeline_factory = pipeline_factory or CaptchaPipeline
|
||||
fun_captcha_pipelines: dict[str, object] = {}
|
||||
load_fun_captcha_on_startup = funcaptcha_factories is None
|
||||
if funcaptcha_factories is None:
|
||||
funcaptcha_factories = {
|
||||
"4_3d_rollball_animals": lambda: FunCaptchaRollballPipeline(
|
||||
question="4_3d_rollball_animals"
|
||||
),
|
||||
}
|
||||
|
||||
# ---- 启动时加载模型 ----
|
||||
@app.on_event("startup")
|
||||
def _load_models():
|
||||
nonlocal pipeline
|
||||
try:
|
||||
pipeline = CaptchaPipeline()
|
||||
except FileNotFoundError:
|
||||
pipeline = None
|
||||
def _infer(image_bytes: bytes, captcha_type: str | None, question: str | None) -> dict:
|
||||
normalized_question = _normalize_question(question)
|
||||
normalized_type = None if normalized_question is not None else _normalize_captcha_type(captcha_type)
|
||||
|
||||
# ---- 请求体定义 ----
|
||||
class SolveRequest(BaseModel):
|
||||
image: str # base64 编码的图片
|
||||
type: Optional[str] = None # 指定类型可跳过分类
|
||||
|
||||
# ---- 通用推理逻辑 ----
|
||||
def _solve(image_bytes: bytes, captcha_type: Optional[str]) -> dict:
|
||||
if pipeline is None:
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={"error": "模型未加载,请先训练并导出 ONNX 模型"},
|
||||
)
|
||||
if normalized_question is None:
|
||||
raise ServiceError(
|
||||
"ERROR_NO_MODELS_LOADED",
|
||||
"模型未加载,请先训练并导出 ONNX 模型",
|
||||
status_code=503,
|
||||
)
|
||||
if not image_bytes:
|
||||
return JSONResponse(status_code=400, content={"error": "空图片"})
|
||||
raise ServiceError("ERROR_EMPTY_IMAGE", "空图片", status_code=400)
|
||||
|
||||
try:
|
||||
result = pipeline.solve(image_bytes, captcha_type=captcha_type)
|
||||
except (RuntimeError, TypeError) as e:
|
||||
return JSONResponse(status_code=400, content={"error": str(e)})
|
||||
except Exception as e:
|
||||
return JSONResponse(status_code=400, content={"error": f"图片解析失败: {e}"})
|
||||
if normalized_question is not None:
|
||||
fun_pipeline = fun_captcha_pipelines.get(normalized_question)
|
||||
if fun_pipeline is None:
|
||||
raise ServiceError(
|
||||
"ERROR_NO_MODELS_LOADED",
|
||||
f"专项模型未加载: {normalized_question}",
|
||||
status_code=503,
|
||||
)
|
||||
result = fun_pipeline.solve(image_bytes)
|
||||
else:
|
||||
result = pipeline.solve(image_bytes, captcha_type=normalized_type)
|
||||
except ServiceError:
|
||||
raise
|
||||
except (RuntimeError, TypeError) as exc:
|
||||
raise ServiceError(
|
||||
"ERROR_INFERENCE_FAILED",
|
||||
str(exc),
|
||||
status_code=400,
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
raise ServiceError(
|
||||
"ERROR_IMAGE_DECODE_FAILED",
|
||||
f"图片解析失败: {exc}",
|
||||
status_code=400,
|
||||
) from exc
|
||||
|
||||
return {
|
||||
payload = {
|
||||
"type": result["type"],
|
||||
"result": result["result"],
|
||||
"raw": result["raw"],
|
||||
"time_ms": result["time_ms"],
|
||||
}
|
||||
if "question" in result:
|
||||
payload["question"] = result["question"]
|
||||
if "objects" in result:
|
||||
payload["objects"] = result["objects"]
|
||||
if "scores" in result:
|
||||
payload["scores"] = result["scores"]
|
||||
return payload
|
||||
|
||||
task_manager = TaskManager(
|
||||
solve_fn=_infer,
|
||||
ttl_seconds=SERVER_CONFIG["task_ttl_seconds"],
|
||||
max_workers=SERVER_CONFIG["task_workers"],
|
||||
tasks_dir=SERVER_CONFIG["tasks_dir"],
|
||||
)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
nonlocal pipeline
|
||||
if load_pipeline_on_startup:
|
||||
try:
|
||||
pipeline = pipeline_factory()
|
||||
except FileNotFoundError:
|
||||
pipeline = None
|
||||
if load_fun_captcha_on_startup:
|
||||
for question, factory in funcaptcha_factories.items():
|
||||
try:
|
||||
fun_captcha_pipelines[question] = factory()
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
task_manager.shutdown()
|
||||
|
||||
app = FastAPI(
|
||||
title="CaptchaBreaker",
|
||||
description="验证码识别多模型系统 - HTTP 推理服务",
|
||||
version="0.2.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.state.task_manager = task_manager
|
||||
|
||||
if not load_pipeline_on_startup:
|
||||
pipeline = pipeline_factory()
|
||||
if not load_fun_captcha_on_startup:
|
||||
fun_captcha_pipelines = {
|
||||
question: factory()
|
||||
for question, factory in funcaptcha_factories.items()
|
||||
}
|
||||
|
||||
# ---- 请求体定义 ----
|
||||
class SolveRequest(BaseModel):
|
||||
image: str
|
||||
type: Optional[str] = None
|
||||
question: Optional[str] = None
|
||||
|
||||
class AsyncTaskRequest(BaseModel):
|
||||
type: str
|
||||
body: Optional[str] = None
|
||||
image: Optional[str] = None
|
||||
captchaType: Optional[str] = None
|
||||
question: Optional[str] = None
|
||||
|
||||
class CreateTaskRequest(BaseModel):
|
||||
clientKey: Optional[str] = None
|
||||
callbackUrl: Optional[str] = None
|
||||
softId: Optional[int] = None
|
||||
languagePool: Optional[str] = None
|
||||
task: AsyncTaskRequest
|
||||
|
||||
class GetTaskResultRequest(BaseModel):
|
||||
clientKey: Optional[str] = None
|
||||
taskId: str
|
||||
|
||||
class BalanceRequest(BaseModel):
|
||||
clientKey: Optional[str] = None
|
||||
|
||||
# ---- 路由 ----
|
||||
@app.get("/health")
|
||||
@app.get("/api/v1/health")
|
||||
def health():
|
||||
models_loaded = pipeline is not None
|
||||
models_loaded = pipeline is not None or bool(fun_captcha_pipelines)
|
||||
return {
|
||||
"status": "ok" if models_loaded else "no_models",
|
||||
"models_loaded": models_loaded,
|
||||
"async_tasks": task_manager.stats(),
|
||||
"client_key_required": bool(SERVER_CONFIG.get("client_key")),
|
||||
"supported_task_types": sorted(ASYNC_TASK_TYPES),
|
||||
"supported_task_questions": sorted(FUN_CAPTCHA_TASKS),
|
||||
}
|
||||
|
||||
@app.post("/solve")
|
||||
@app.post("/api/v1/solve")
|
||||
async def solve_base64(req: SolveRequest):
|
||||
"""JSON 请求,图片通过 base64 传输。"""
|
||||
try:
|
||||
image_bytes = base64.b64decode(req.image)
|
||||
except Exception:
|
||||
image_bytes = _decode_image_b64(req.image)
|
||||
return _infer(image_bytes, req.type, getattr(req, "question", None))
|
||||
except ServiceError as exc:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"error": "base64 解码失败"},
|
||||
status_code=exc.status_code,
|
||||
content={"error": exc.description, "error_code": exc.code},
|
||||
)
|
||||
return _solve(image_bytes, req.type)
|
||||
|
||||
@app.post("/solve/upload")
|
||||
@app.post("/api/v1/solve/upload")
|
||||
async def solve_upload(
|
||||
image: UploadFile = File(...),
|
||||
type: Optional[str] = Query(None, description="指定类型跳过分类"),
|
||||
question: Optional[str] = Query(None, description="专项 question"),
|
||||
):
|
||||
"""multipart 文件上传。"""
|
||||
data = await image.read()
|
||||
return _solve(data, type)
|
||||
try:
|
||||
return _infer(await image.read(), type, question)
|
||||
except ServiceError as exc:
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"error": exc.description, "error_code": exc.code},
|
||||
)
|
||||
|
||||
@app.post("/createTask")
|
||||
@app.post("/api/v1/createTask")
|
||||
async def create_task(req: CreateTaskRequest, request: Request):
|
||||
"""创建异步识别任务,返回 taskId。"""
|
||||
task = req.task
|
||||
try:
|
||||
_validate_client_key(req.clientKey)
|
||||
except ServiceError as exc:
|
||||
return _task_error_payload(exc.code, exc.description)
|
||||
|
||||
if task.type not in ASYNC_TASK_TYPES:
|
||||
return _task_error_payload(
|
||||
"ERROR_TASK_TYPE_NOT_SUPPORTED",
|
||||
f"不支持的任务类型: {task.type}",
|
||||
)
|
||||
|
||||
try:
|
||||
question = _normalize_question(getattr(task, "question", None))
|
||||
if question is None:
|
||||
_normalize_captcha_type(task.captchaType)
|
||||
image_bytes = _decode_image_b64(task.body or task.image or "")
|
||||
if question is None and pipeline is None:
|
||||
raise ServiceError(
|
||||
"ERROR_NO_MODELS_LOADED",
|
||||
"模型未加载,请先训练并导出 ONNX 模型",
|
||||
status_code=503,
|
||||
)
|
||||
if question is not None and question not in fun_captcha_pipelines:
|
||||
raise ServiceError(
|
||||
"ERROR_NO_MODELS_LOADED",
|
||||
f"专项模型未加载: {question}",
|
||||
status_code=503,
|
||||
)
|
||||
except ServiceError as exc:
|
||||
return _task_error_payload(exc.code, exc.description)
|
||||
|
||||
client = getattr(request, "client", None)
|
||||
client_ip = getattr(client, "host", None)
|
||||
task_id = task_manager.create_task(
|
||||
image_bytes,
|
||||
task.captchaType,
|
||||
question=question,
|
||||
client_ip=client_ip,
|
||||
task_type=task.type,
|
||||
callback_url=getattr(req, "callbackUrl", None),
|
||||
)
|
||||
task = task_manager.get_task(task_id)
|
||||
return _task_success_payload(
|
||||
taskId=task_id,
|
||||
status="processing",
|
||||
createTime=task.created_at if task else int(time.time()),
|
||||
expiresAt=task.expires_at if task else None,
|
||||
)
|
||||
|
||||
@app.post("/getTaskResult")
|
||||
@app.post("/api/v1/getTaskResult")
|
||||
async def get_task_result(req: GetTaskResultRequest):
|
||||
"""按 taskId 查询任务状态与结果。"""
|
||||
try:
|
||||
_validate_client_key(req.clientKey)
|
||||
except ServiceError as exc:
|
||||
return _task_error_payload(exc.code, exc.description, taskId=req.taskId)
|
||||
|
||||
task = task_manager.get_task(str(req.taskId))
|
||||
if task is None:
|
||||
return _task_error_payload(
|
||||
"ERROR_TASK_NOT_FOUND",
|
||||
f"任务不存在或已过期: {req.taskId}",
|
||||
taskId=req.taskId,
|
||||
)
|
||||
|
||||
return _build_task_result_payload(task)
|
||||
|
||||
@app.post("/getBalance")
|
||||
@app.post("/api/v1/getBalance")
|
||||
async def get_balance(req: BalanceRequest | None = Body(default=None)):
|
||||
try:
|
||||
_validate_client_key(getattr(req, "clientKey", None))
|
||||
except ServiceError as exc:
|
||||
return _task_error_payload(exc.code, exc.description)
|
||||
return _task_success_payload(balance=SERVER_CONFIG["balance"])
|
||||
|
||||
return app
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
三种求解方法 (按优先级):
|
||||
1. 模板匹配: 背景图 + 模板图 → Canny → matchTemplate
|
||||
2. 边缘检测: 单图 Canny → findContours → 筛选方形轮廓
|
||||
3. CNN 兜底: ONNX 推理 → sigmoid → x 百分比 → 像素
|
||||
3. CNN 兜底: ONNX 推理 → sigmoid → 缺口中心 x 百分比 → 像素
|
||||
|
||||
OpenCV 延迟导入,未安装时退化到 CNN only。
|
||||
"""
|
||||
@@ -60,6 +60,7 @@ class SlideSolver(BaseSolver):
|
||||
|
||||
Returns:
|
||||
{"gap_x": int, "gap_x_percent": float, "confidence": float, "method": str}
|
||||
其中 gap_x 统一表示缺口中心点的 x 坐标。
|
||||
"""
|
||||
bg = self._load_image(bg_image)
|
||||
|
||||
@@ -168,12 +169,11 @@ class SlideSolver(BaseSolver):
|
||||
arr = arr[np.newaxis, np.newaxis, :, :] # (1, 1, H, W)
|
||||
|
||||
outputs = self._onnx_session.run(None, {"input": arr})
|
||||
percent = float(outputs[0][0][0])
|
||||
|
||||
gap_x = int(percent * orig_w)
|
||||
center_ratio = float(outputs[0][0][0])
|
||||
gap_x = int(center_ratio * orig_w)
|
||||
return {
|
||||
"gap_x": gap_x,
|
||||
"gap_x_percent": percent,
|
||||
"gap_x_percent": center_ratio,
|
||||
"confidence": 0.5, # CNN 无置信度
|
||||
"method": "cnn",
|
||||
}
|
||||
|
||||
109
tests/test_data_fingerprint.py
Normal file
109
tests/test_data_fingerprint.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
测试合成数据指纹与 ONNX metadata 辅助逻辑。
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from inference.model_metadata import load_model_metadata, write_model_metadata
|
||||
from training.data_fingerprint import (
|
||||
build_dataset_spec,
|
||||
ensure_synthetic_dataset,
|
||||
load_dataset_manifest,
|
||||
)
|
||||
|
||||
|
||||
class AdoptableGenerator:
|
||||
def generate_dataset(self, num_samples: int, output_dir: str):
|
||||
out_dir = Path(output_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
for idx in range(num_samples):
|
||||
Image.new("L", (2, 2), color=255).save(out_dir / f"kept_{idx:06d}.png")
|
||||
|
||||
|
||||
class RefreshingGenerator:
|
||||
def generate_dataset(self, num_samples: int, output_dir: str):
|
||||
out_dir = Path(output_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
for idx in range(num_samples):
|
||||
Image.new("L", (2, 2), color=255).save(out_dir / f"20÷4_{idx:06d}.png")
|
||||
|
||||
|
||||
class TestDataFingerprint:
|
||||
def test_adopt_existing_dataset_when_manifest_missing(self, tmp_path):
|
||||
dataset_dir = tmp_path / "normal"
|
||||
dataset_dir.mkdir()
|
||||
for idx, label in enumerate(["AB12", "CD34"]):
|
||||
Image.new("L", (2, 2), color=255).save(dataset_dir / f"{label}_{idx:06d}.png")
|
||||
|
||||
state = ensure_synthetic_dataset(
|
||||
dataset_dir,
|
||||
generator_cls=AdoptableGenerator,
|
||||
spec=build_dataset_spec(
|
||||
AdoptableGenerator,
|
||||
config_key="normal",
|
||||
config_snapshot={"chars": "ABCD1234"},
|
||||
),
|
||||
gen_count=2,
|
||||
exact_count=2,
|
||||
adopt_if_missing=True,
|
||||
)
|
||||
|
||||
manifest = load_dataset_manifest(dataset_dir)
|
||||
assert state["adopted"] is True
|
||||
assert state["refreshed"] is False
|
||||
assert manifest is not None
|
||||
assert manifest["adopted_existing"] is True
|
||||
assert manifest["sample_count"] == 2
|
||||
|
||||
def test_refresh_dataset_when_validator_fails(self, tmp_path):
|
||||
dataset_dir = tmp_path / "math"
|
||||
dataset_dir.mkdir()
|
||||
for idx, label in enumerate(["1+1", "2-1"]):
|
||||
Image.new("L", (2, 2), color=255).save(dataset_dir / f"{label}_{idx:06d}.png")
|
||||
|
||||
state = ensure_synthetic_dataset(
|
||||
dataset_dir,
|
||||
generator_cls=RefreshingGenerator,
|
||||
spec=build_dataset_spec(
|
||||
RefreshingGenerator,
|
||||
config_key="math",
|
||||
config_snapshot={"operators": ["+", "-", "÷"]},
|
||||
),
|
||||
gen_count=2,
|
||||
exact_count=2,
|
||||
validator=lambda files: any("÷" in path.stem for path in files),
|
||||
adopt_if_missing=True,
|
||||
)
|
||||
|
||||
files = sorted(dataset_dir.glob("*.png"))
|
||||
manifest = load_dataset_manifest(dataset_dir)
|
||||
assert state["refreshed"] is True
|
||||
assert state["adopted"] is False
|
||||
assert manifest is not None
|
||||
assert manifest["adopted_existing"] is False
|
||||
assert len(files) == 2
|
||||
assert all("÷" in path.stem for path in files)
|
||||
|
||||
|
||||
class TestModelMetadata:
|
||||
def test_write_and_load_model_metadata(self, tmp_path):
|
||||
model_path = tmp_path / "normal.onnx"
|
||||
model_path.touch()
|
||||
|
||||
write_model_metadata(
|
||||
model_path,
|
||||
{
|
||||
"model_name": "normal",
|
||||
"task": "ctc",
|
||||
"chars": "ABC",
|
||||
"input_shape": [1, 40, 120],
|
||||
},
|
||||
)
|
||||
|
||||
metadata = load_model_metadata(model_path)
|
||||
assert metadata is not None
|
||||
assert metadata["version"] == 1
|
||||
assert metadata["chars"] == "ABC"
|
||||
assert metadata["task"] == "ctc"
|
||||
123
tests/test_funcaptcha.py
Normal file
123
tests/test_funcaptcha.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
from config import FUN_CAPTCHA_TASKS, IMAGE_SIZE
|
||||
import inference.fun_captcha as fun_module
|
||||
from inference.fun_captcha import FunCaptchaRollballPipeline
|
||||
from inference.model_metadata import write_model_metadata
|
||||
from models.fun_captcha_siamese import FunCaptchaSiamese
|
||||
from training.dataset import FunCaptchaChallengeDataset, build_val_rgb_transform
|
||||
|
||||
|
||||
def _build_rollball_image(path: Path, answer_idx: int = 2):
|
||||
colors = [
|
||||
(255, 80, 80),
|
||||
(80, 255, 80),
|
||||
(80, 80, 255),
|
||||
(255, 220, 80),
|
||||
]
|
||||
image = Image.new("RGB", (800, 400), color=(245, 245, 245))
|
||||
for idx, color in enumerate(colors):
|
||||
tile = Image.new("RGB", (200, 200), color=color)
|
||||
image.paste(tile, (idx * 200, 0))
|
||||
|
||||
reference = Image.new("RGB", (200, 200), color=colors[answer_idx])
|
||||
image.paste(reference, (0, 200))
|
||||
image.save(path)
|
||||
|
||||
|
||||
class TestFunCaptchaChallengeDataset:
|
||||
def test_dataset_splits_candidates_and_reference(self, tmp_path):
|
||||
sample_path = tmp_path / "2_demo.png"
|
||||
_build_rollball_image(sample_path, answer_idx=2)
|
||||
|
||||
dataset = FunCaptchaChallengeDataset(
|
||||
dirs=[tmp_path],
|
||||
task_config=FUN_CAPTCHA_TASKS["4_3d_rollball_animals"],
|
||||
transform=build_val_rgb_transform(*IMAGE_SIZE["funcaptcha_rollball_animals"]),
|
||||
)
|
||||
|
||||
candidates, reference, answer_idx = dataset[0]
|
||||
assert candidates.shape == (4, 3, 48, 48)
|
||||
assert reference.shape == (3, 48, 48)
|
||||
assert int(answer_idx.item()) == 2
|
||||
|
||||
|
||||
class TestFunCaptchaSiamese:
|
||||
def test_forward_shape(self):
|
||||
model = FunCaptchaSiamese()
|
||||
model.eval()
|
||||
candidate = torch.randn(5, 3, 48, 48)
|
||||
reference = torch.randn(5, 3, 48, 48)
|
||||
out = model(candidate, reference)
|
||||
assert out.shape == (5, 1)
|
||||
|
||||
def test_param_count_reasonable(self):
|
||||
model = FunCaptchaSiamese()
|
||||
n = sum(p.numel() for p in model.parameters())
|
||||
assert n < 450_000, f"too many params: {n}"
|
||||
|
||||
|
||||
class _FakeSessionOptions:
|
||||
def __init__(self):
|
||||
self.inter_op_num_threads = 0
|
||||
self.intra_op_num_threads = 0
|
||||
|
||||
|
||||
class _FakeInput:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
|
||||
class _FakeSession:
|
||||
def __init__(self, path, *args, **kwargs):
|
||||
self.path = path
|
||||
|
||||
def get_inputs(self):
|
||||
return [_FakeInput("candidate"), _FakeInput("reference")]
|
||||
|
||||
def run(self, output_names, feed_dict):
|
||||
batch_size = next(iter(feed_dict.values())).shape[0]
|
||||
logits = np.full((batch_size, 1), 0.1, dtype=np.float32)
|
||||
if batch_size >= 3:
|
||||
logits[2, 0] = 0.95
|
||||
return [logits]
|
||||
|
||||
|
||||
class _FakeOrt:
|
||||
SessionOptions = _FakeSessionOptions
|
||||
InferenceSession = _FakeSession
|
||||
|
||||
|
||||
class TestFunCaptchaPipeline:
|
||||
def test_pipeline_returns_best_object_index(self, tmp_path, monkeypatch):
|
||||
model_path = tmp_path / "funcaptcha_rollball_animals.onnx"
|
||||
model_path.touch()
|
||||
write_model_metadata(
|
||||
model_path,
|
||||
{
|
||||
"model_name": "funcaptcha_rollball_animals",
|
||||
"task": "funcaptcha_siamese",
|
||||
"question": "4_3d_rollball_animals",
|
||||
"num_candidates": 4,
|
||||
"tile_size": [200, 200],
|
||||
"reference_box": [0, 200, 200, 400],
|
||||
"answer_index_base": 0,
|
||||
"input_shape": [3, 48, 48],
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(fun_module, "_try_import_ort", lambda: _FakeOrt)
|
||||
|
||||
sample_path = tmp_path / "1_demo.png"
|
||||
_build_rollball_image(sample_path, answer_idx=1)
|
||||
|
||||
pipeline = FunCaptchaRollballPipeline(models_dir=tmp_path)
|
||||
result = pipeline.solve(sample_path)
|
||||
assert result["question"] == "4_3d_rollball_animals"
|
||||
assert result["objects"] == [2]
|
||||
assert result["result"] == "2"
|
||||
assert len(result["scores"]) == 4
|
||||
@@ -9,7 +9,14 @@ import re
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from config import GENERATE_CONFIG, NORMAL_CHARS, MATH_CHARS, THREED_CHARS, SOLVER_CONFIG
|
||||
from config import (
|
||||
GENERATE_CONFIG,
|
||||
NORMAL_CHARS,
|
||||
MATH_CHARS,
|
||||
THREED_CHARS,
|
||||
SOLVER_CONFIG,
|
||||
SOLVER_REGRESSION_RANGE,
|
||||
)
|
||||
from generators import (
|
||||
NormalCaptchaGenerator,
|
||||
MathCaptchaGenerator,
|
||||
@@ -67,6 +74,10 @@ class TestMathCaptchaGenerator:
|
||||
img, label = self.gen.generate()
|
||||
assert re.match(r"^\d+[+\-×÷]\d+$", label), f"unexpected label format: {label!r}"
|
||||
|
||||
def test_generate_with_division_text(self):
|
||||
img, label = self.gen.generate(text="20÷4")
|
||||
assert label == "20÷4"
|
||||
|
||||
|
||||
class TestThreeDCaptchaGenerator:
|
||||
def setup_method(self):
|
||||
@@ -150,7 +161,25 @@ class TestSlideDataGenerator:
|
||||
def test_label_is_numeric(self):
|
||||
img, label = self.gen.generate()
|
||||
val = int(label)
|
||||
assert val >= 0
|
||||
gs = self.gen.gap_size
|
||||
margin = gs + 10
|
||||
assert margin + gs // 2 <= val <= self.gen.width - margin + gs // 2
|
||||
|
||||
def test_labels_normalize_inside_solver_range(self, tmp_path):
|
||||
for idx in range(3):
|
||||
img, label = self.gen.generate()
|
||||
img.save(tmp_path / f"{label}_{idx:06d}.png")
|
||||
|
||||
from training.dataset import RegressionDataset
|
||||
|
||||
ds = RegressionDataset(
|
||||
dirs=[tmp_path],
|
||||
label_range=SOLVER_REGRESSION_RANGE["slide"],
|
||||
transform=None,
|
||||
)
|
||||
assert len(ds.samples) == 3
|
||||
for _, norm in ds.samples:
|
||||
assert 0.0 < norm < 1.0
|
||||
|
||||
|
||||
class TestRotateSolverDataGenerator:
|
||||
|
||||
@@ -8,11 +8,15 @@
|
||||
"""
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from inference.math_eval import eval_captcha_math
|
||||
from inference.model_metadata import write_model_metadata
|
||||
import inference.pipeline as pipeline_module
|
||||
from inference.pipeline import CaptchaPipeline
|
||||
|
||||
|
||||
@@ -97,6 +101,96 @@ class TestCTCGreedyDecode:
|
||||
assert result == "AA"
|
||||
|
||||
|
||||
class _FakeInput:
|
||||
name = "input"
|
||||
|
||||
|
||||
class _FakeSession:
|
||||
def __init__(self, path, *args, **kwargs):
|
||||
self.model_name = Path(path).name
|
||||
|
||||
def get_inputs(self):
|
||||
return [_FakeInput()]
|
||||
|
||||
def run(self, output_names, feed_dict):
|
||||
if self.model_name == "classifier.onnx":
|
||||
return [np.array([[0.1, 0.9]], dtype=np.float32)]
|
||||
if self.model_name == "normal.onnx":
|
||||
logits = np.full((2, 1, 4), -10.0, dtype=np.float32)
|
||||
logits[0, 0, 2] = 10.0
|
||||
logits[1, 0, 0] = 10.0
|
||||
return [logits]
|
||||
if self.model_name == "threed_rotate.onnx":
|
||||
return [np.array([[0.25]], dtype=np.float32)]
|
||||
raise AssertionError(f"unexpected fake session: {self.model_name}")
|
||||
|
||||
|
||||
class _FakeSessionOptions:
|
||||
def __init__(self):
|
||||
self.inter_op_num_threads = 0
|
||||
self.intra_op_num_threads = 0
|
||||
|
||||
|
||||
class _FakeOrt:
|
||||
SessionOptions = _FakeSessionOptions
|
||||
InferenceSession = _FakeSession
|
||||
|
||||
|
||||
class TestPipelineMetadata:
|
||||
def test_classifier_uses_metadata_class_order(self, tmp_path, monkeypatch):
|
||||
(tmp_path / "classifier.onnx").touch()
|
||||
write_model_metadata(
|
||||
tmp_path / "classifier.onnx",
|
||||
{
|
||||
"model_name": "classifier",
|
||||
"task": "classifier",
|
||||
"class_names": ["math", "normal"],
|
||||
"input_shape": [1, 64, 128],
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(pipeline_module, "_try_import_ort", lambda: _FakeOrt)
|
||||
|
||||
pipeline = CaptchaPipeline(models_dir=tmp_path)
|
||||
captcha_type = pipeline.classify(Image.new("RGB", (32, 32), color="white"))
|
||||
assert captcha_type == "normal"
|
||||
|
||||
def test_solve_uses_ctc_chars_metadata(self, tmp_path, monkeypatch):
|
||||
(tmp_path / "normal.onnx").touch()
|
||||
write_model_metadata(
|
||||
tmp_path / "normal.onnx",
|
||||
{
|
||||
"model_name": "normal",
|
||||
"task": "ctc",
|
||||
"chars": "XYZ",
|
||||
"input_shape": [1, 40, 120],
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(pipeline_module, "_try_import_ort", lambda: _FakeOrt)
|
||||
|
||||
pipeline = CaptchaPipeline(models_dir=tmp_path)
|
||||
result = pipeline.solve(Image.new("RGB", (32, 32), color="white"), captcha_type="normal")
|
||||
assert result["raw"] == "Y"
|
||||
assert result["result"] == "Y"
|
||||
|
||||
def test_solve_uses_regression_label_range_metadata(self, tmp_path, monkeypatch):
|
||||
(tmp_path / "threed_rotate.onnx").touch()
|
||||
write_model_metadata(
|
||||
tmp_path / "threed_rotate.onnx",
|
||||
{
|
||||
"model_name": "threed_rotate",
|
||||
"task": "regression",
|
||||
"label_range": [100, 200],
|
||||
"input_shape": [1, 80, 80],
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(pipeline_module, "_try_import_ort", lambda: _FakeOrt)
|
||||
|
||||
pipeline = CaptchaPipeline(models_dir=tmp_path)
|
||||
result = pipeline.solve(Image.new("RGB", (32, 32), color="white"), captcha_type="3d_rotate")
|
||||
assert result["raw"] == "125.0"
|
||||
assert result["result"] == "125"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SlideSolver 测试
|
||||
# ============================================================
|
||||
|
||||
423
tests/test_server.py
Normal file
423
tests/test_server.py
Normal file
@@ -0,0 +1,423 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import time
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from urllib.error import URLError
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from config import SERVER_CONFIG
|
||||
import server as server_module
|
||||
from server import create_app
|
||||
|
||||
|
||||
class _FakePipeline:
|
||||
def solve(self, image, captcha_type=None):
|
||||
return {
|
||||
"type": captcha_type or "normal",
|
||||
"result": "A3B8",
|
||||
"raw": "A3B8",
|
||||
"time_ms": 1.23,
|
||||
}
|
||||
|
||||
|
||||
class _FakeFunPipeline:
|
||||
def solve(self, image):
|
||||
return {
|
||||
"type": "funcaptcha",
|
||||
"question": "4_3d_rollball_animals",
|
||||
"objects": [2],
|
||||
"result": "2",
|
||||
"raw": "2",
|
||||
"time_ms": 2.34,
|
||||
}
|
||||
|
||||
|
||||
def _create_test_app(funcaptcha_factories=None):
|
||||
return create_app(
|
||||
pipeline_factory=_FakePipeline,
|
||||
funcaptcha_factories=funcaptcha_factories,
|
||||
)
|
||||
|
||||
|
||||
def _get_route(app, path: str):
|
||||
for route in app.routes:
|
||||
if getattr(route, "path", None) == path:
|
||||
return route.endpoint
|
||||
raise AssertionError(f"route not found: {path}")
|
||||
|
||||
|
||||
def _fake_request(host: str = "127.0.0.1"):
|
||||
return SimpleNamespace(client=SimpleNamespace(host=host))
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_server_config(monkeypatch, tmp_path):
|
||||
monkeypatch.setitem(SERVER_CONFIG, "client_key", None)
|
||||
monkeypatch.setitem(SERVER_CONFIG, "tasks_dir", str(tmp_path / "server_tasks"))
|
||||
monkeypatch.setitem(SERVER_CONFIG, "task_cost", 0.0)
|
||||
monkeypatch.setitem(SERVER_CONFIG, "callback_max_retries", 2)
|
||||
monkeypatch.setitem(SERVER_CONFIG, "callback_retry_delay_seconds", 1.0)
|
||||
monkeypatch.setitem(SERVER_CONFIG, "callback_retry_backoff", 2.0)
|
||||
monkeypatch.setitem(SERVER_CONFIG, "callback_signing_secret", None)
|
||||
|
||||
|
||||
def test_solve_base64_returns_sync_payload():
|
||||
app = _create_test_app()
|
||||
solve = _get_route(app, "/api/v1/solve")
|
||||
encoded = base64.b64encode(b"fake-image-bytes").decode("ascii")
|
||||
|
||||
response = asyncio.run(
|
||||
solve(SimpleNamespace(image=encoded, type="math"))
|
||||
)
|
||||
|
||||
assert response == {
|
||||
"type": "math",
|
||||
"result": "A3B8",
|
||||
"raw": "A3B8",
|
||||
"time_ms": 1.23,
|
||||
}
|
||||
|
||||
|
||||
def test_create_task_and_get_task_result():
|
||||
app = _create_test_app()
|
||||
create_task = _get_route(app, "/createTask")
|
||||
get_task_result = _get_route(app, "/getTaskResult")
|
||||
encoded = base64.b64encode(b"fake-image-bytes").decode("ascii")
|
||||
|
||||
create_response = asyncio.run(
|
||||
create_task(
|
||||
SimpleNamespace(
|
||||
clientKey="local",
|
||||
task=SimpleNamespace(
|
||||
type="ImageToTextTaskM1",
|
||||
body=encoded,
|
||||
image=None,
|
||||
captchaType="normal",
|
||||
),
|
||||
),
|
||||
_fake_request("10.0.0.8"),
|
||||
)
|
||||
)
|
||||
|
||||
assert create_response["errorId"] == 0
|
||||
assert create_response["status"] == "processing"
|
||||
assert isinstance(create_response["createTime"], int)
|
||||
assert isinstance(create_response["expiresAt"], int)
|
||||
task_id = create_response["taskId"]
|
||||
|
||||
result_response = None
|
||||
for _ in range(20):
|
||||
result_response = asyncio.run(
|
||||
get_task_result(SimpleNamespace(clientKey="local", taskId=task_id))
|
||||
)
|
||||
if result_response.get("status") == "ready":
|
||||
break
|
||||
|
||||
assert result_response is not None
|
||||
assert result_response["errorId"] == 0
|
||||
assert result_response["status"] == "ready"
|
||||
assert result_response["solution"] == {
|
||||
"text": "A3B8",
|
||||
"answer": "A3B8",
|
||||
"raw": "A3B8",
|
||||
"captchaType": "normal",
|
||||
"timeMs": 1.23,
|
||||
}
|
||||
assert result_response["cost"] == "0.00000"
|
||||
assert result_response["ip"] == "10.0.0.8"
|
||||
assert result_response["solveCount"] == 1
|
||||
assert result_response["task"] == {
|
||||
"type": "ImageToTextTaskM1",
|
||||
"captchaType": "normal",
|
||||
}
|
||||
assert result_response["callback"] == {
|
||||
"configured": False,
|
||||
"url": None,
|
||||
"attempts": 0,
|
||||
"delivered": False,
|
||||
"deliveredAt": None,
|
||||
"lastError": None,
|
||||
}
|
||||
assert isinstance(result_response["expiresAt"], int)
|
||||
|
||||
|
||||
def test_get_task_result_returns_not_found_for_unknown_task():
|
||||
app = _create_test_app()
|
||||
get_task_result = _get_route(app, "/getTaskResult")
|
||||
|
||||
response = asyncio.run(
|
||||
get_task_result(SimpleNamespace(clientKey="local", taskId="missing-task"))
|
||||
)
|
||||
|
||||
assert response["errorCode"] == "ERROR_TASK_NOT_FOUND"
|
||||
|
||||
|
||||
def test_solve_returns_json_error_for_invalid_base64():
|
||||
app = _create_test_app()
|
||||
solve = _get_route(app, "/solve")
|
||||
|
||||
response = asyncio.run(
|
||||
solve(SimpleNamespace(image="not_base64!", type="normal"))
|
||||
)
|
||||
|
||||
assert isinstance(response, JSONResponse)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_health_alias_reports_client_key_flag(monkeypatch):
|
||||
app = _create_test_app()
|
||||
health = _get_route(app, "/api/v1/health")
|
||||
|
||||
monkeypatch.setitem(SERVER_CONFIG, "client_key", "secret")
|
||||
response = health()
|
||||
|
||||
assert response["status"] == "ok"
|
||||
assert response["client_key_required"] is True
|
||||
assert "ImageToTextTask" in response["supported_task_types"]
|
||||
|
||||
|
||||
def test_client_key_is_required_for_task_api(monkeypatch):
|
||||
app = _create_test_app()
|
||||
create_task = _get_route(app, "/api/v1/createTask")
|
||||
get_balance = _get_route(app, "/api/v1/getBalance")
|
||||
encoded = base64.b64encode(b"fake-image-bytes").decode("ascii")
|
||||
|
||||
monkeypatch.setitem(SERVER_CONFIG, "client_key", "secret")
|
||||
|
||||
create_response = asyncio.run(
|
||||
create_task(
|
||||
SimpleNamespace(
|
||||
clientKey="wrong-key",
|
||||
task=SimpleNamespace(
|
||||
type="ImageToTextTask",
|
||||
body=encoded,
|
||||
image=None,
|
||||
captchaType="normal",
|
||||
),
|
||||
),
|
||||
_fake_request(),
|
||||
)
|
||||
)
|
||||
balance_response = asyncio.run(
|
||||
get_balance(SimpleNamespace(clientKey="wrong-key"))
|
||||
)
|
||||
|
||||
assert create_response["errorCode"] == "ERROR_KEY_DOES_NOT_EXIST"
|
||||
assert balance_response["errorCode"] == "ERROR_KEY_DOES_NOT_EXIST"
|
||||
|
||||
|
||||
def test_create_task_triggers_callback(monkeypatch):
|
||||
app = _create_test_app()
|
||||
create_task = _get_route(app, "/createTask")
|
||||
callbacks = []
|
||||
encoded = base64.b64encode(b"fake-image-bytes").decode("ascii")
|
||||
|
||||
def _fake_post_callback(callback_url, payload):
|
||||
callbacks.append((callback_url, payload))
|
||||
|
||||
monkeypatch.setattr(server_module, "_post_callback", _fake_post_callback)
|
||||
|
||||
response = asyncio.run(
|
||||
create_task(
|
||||
SimpleNamespace(
|
||||
clientKey="local",
|
||||
callbackUrl="https://example.com/callback",
|
||||
task=SimpleNamespace(
|
||||
type="ImageToTextTask",
|
||||
body=encoded,
|
||||
image=None,
|
||||
captchaType="normal",
|
||||
),
|
||||
),
|
||||
_fake_request("10.0.0.9"),
|
||||
)
|
||||
)
|
||||
|
||||
assert response["errorId"] == 0
|
||||
|
||||
for _ in range(20):
|
||||
if callbacks:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
|
||||
assert callbacks == [
|
||||
(
|
||||
"https://example.com/callback",
|
||||
{
|
||||
"id": response["taskId"],
|
||||
"taskId": response["taskId"],
|
||||
"status": "ready",
|
||||
"errorId": "0",
|
||||
"code": "A3B8",
|
||||
"text": "A3B8",
|
||||
"answer": "A3B8",
|
||||
"raw": "A3B8",
|
||||
"captchaType": "normal",
|
||||
"timeMs": "1.23",
|
||||
"cost": "0.00000",
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_create_task_routes_fun_captcha_question():
|
||||
app = _create_test_app(
|
||||
funcaptcha_factories={"4_3d_rollball_animals": _FakeFunPipeline}
|
||||
)
|
||||
create_task = _get_route(app, "/createTask")
|
||||
get_task_result = _get_route(app, "/getTaskResult")
|
||||
encoded = base64.b64encode(b"fake-image-bytes").decode("ascii")
|
||||
|
||||
create_response = asyncio.run(
|
||||
create_task(
|
||||
SimpleNamespace(
|
||||
clientKey="local",
|
||||
task=SimpleNamespace(
|
||||
type="FunCaptcha",
|
||||
body=encoded,
|
||||
image=None,
|
||||
captchaType=None,
|
||||
question="4_3d_rollball_animals",
|
||||
),
|
||||
),
|
||||
_fake_request("10.0.0.7"),
|
||||
)
|
||||
)
|
||||
|
||||
task_id = create_response["taskId"]
|
||||
result_response = None
|
||||
for _ in range(20):
|
||||
result_response = asyncio.run(
|
||||
get_task_result(SimpleNamespace(clientKey="local", taskId=task_id))
|
||||
)
|
||||
if result_response.get("status") == "ready":
|
||||
break
|
||||
|
||||
assert result_response["errorId"] == 0
|
||||
assert result_response["status"] == "ready"
|
||||
assert result_response["solution"] == {
|
||||
"objects": [2],
|
||||
"answer": 2,
|
||||
"raw": "2",
|
||||
"timeMs": 2.34,
|
||||
"question": "4_3d_rollball_animals",
|
||||
"text": "2",
|
||||
}
|
||||
assert result_response["task"] == {
|
||||
"type": "FunCaptcha",
|
||||
"captchaType": None,
|
||||
"question": "4_3d_rollball_animals",
|
||||
}
|
||||
|
||||
|
||||
def test_create_task_retries_callback(monkeypatch):
|
||||
app = _create_test_app()
|
||||
create_task = _get_route(app, "/createTask")
|
||||
attempts = []
|
||||
encoded = base64.b64encode(b"fake-image-bytes").decode("ascii")
|
||||
|
||||
def _flaky_post_callback(callback_url, payload):
|
||||
attempts.append((callback_url, payload["taskId"]))
|
||||
if len(attempts) < 3:
|
||||
raise URLError("temporary failure")
|
||||
|
||||
monkeypatch.setattr(server_module, "_post_callback", _flaky_post_callback)
|
||||
monkeypatch.setitem(SERVER_CONFIG, "callback_retry_delay_seconds", 0.0)
|
||||
monkeypatch.setitem(SERVER_CONFIG, "callback_retry_backoff", 1.0)
|
||||
|
||||
response = asyncio.run(
|
||||
create_task(
|
||||
SimpleNamespace(
|
||||
clientKey="local",
|
||||
callbackUrl="https://example.com/callback",
|
||||
task=SimpleNamespace(
|
||||
type="ImageToTextTask",
|
||||
body=encoded,
|
||||
image=None,
|
||||
captchaType="normal",
|
||||
),
|
||||
),
|
||||
_fake_request(),
|
||||
)
|
||||
)
|
||||
|
||||
for _ in range(20):
|
||||
if len(attempts) >= 3:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
|
||||
assert response["errorId"] == 0
|
||||
assert response["status"] == "processing"
|
||||
assert attempts == [
|
||||
("https://example.com/callback", response["taskId"]),
|
||||
("https://example.com/callback", response["taskId"]),
|
||||
("https://example.com/callback", response["taskId"]),
|
||||
]
|
||||
|
||||
|
||||
def test_tasks_are_restored_from_disk():
|
||||
app = _create_test_app()
|
||||
create_task = _get_route(app, "/createTask")
|
||||
get_task_result = _get_route(app, "/getTaskResult")
|
||||
encoded = base64.b64encode(b"fake-image-bytes").decode("ascii")
|
||||
|
||||
create_response = asyncio.run(
|
||||
create_task(
|
||||
SimpleNamespace(
|
||||
clientKey="local",
|
||||
task=SimpleNamespace(
|
||||
type="ImageToTextTask",
|
||||
body=encoded,
|
||||
image=None,
|
||||
captchaType="normal",
|
||||
),
|
||||
),
|
||||
_fake_request(),
|
||||
)
|
||||
)
|
||||
task_id = create_response["taskId"]
|
||||
|
||||
for _ in range(20):
|
||||
result = asyncio.run(
|
||||
get_task_result(SimpleNamespace(clientKey="local", taskId=task_id))
|
||||
)
|
||||
if result.get("status") == "ready":
|
||||
break
|
||||
time.sleep(0.01)
|
||||
|
||||
task_file = Path(SERVER_CONFIG["tasks_dir"]) / f"{task_id}.json"
|
||||
assert task_file.exists()
|
||||
|
||||
reloaded_app = _create_test_app()
|
||||
reloaded_get_task_result = _get_route(reloaded_app, "/getTaskResult")
|
||||
reloaded_result = asyncio.run(
|
||||
reloaded_get_task_result(SimpleNamespace(clientKey="local", taskId=task_id))
|
||||
)
|
||||
|
||||
assert reloaded_result["status"] == "ready"
|
||||
assert reloaded_result["solution"]["answer"] == "A3B8"
|
||||
|
||||
|
||||
def test_callback_request_includes_signature_headers(monkeypatch):
|
||||
monkeypatch.setitem(SERVER_CONFIG, "callback_signing_secret", "shared-secret")
|
||||
|
||||
request = server_module._build_callback_request(
|
||||
"https://example.com/callback",
|
||||
{"taskId": "abc", "status": "ready"},
|
||||
)
|
||||
|
||||
body = urlencode({"taskId": "abc", "status": "ready"}).encode("utf-8")
|
||||
headers = {key.lower(): value for key, value in request.header_items()}
|
||||
timestamp = headers["x-captchabreaker-timestamp"]
|
||||
signature = headers["x-captchabreaker-signature"]
|
||||
|
||||
assert headers["content-type"] == "application/x-www-form-urlencoded"
|
||||
assert headers["x-captchabreaker-signature-alg"] == "hmac-sha256"
|
||||
assert signature == server_module._sign_callback_payload(body, timestamp, "shared-secret")
|
||||
@@ -12,4 +12,5 @@
|
||||
- train_classifier.py: 训练调度分类器 (CaptchaClassifier)
|
||||
- train_slide.py: 训练滑块缺口检测 (GapDetectorCNN)
|
||||
- train_rotate_solver.py: 训练旋转角度回归 (RotationRegressor)
|
||||
- train_funcaptcha_rollball.py: 训练 FunCaptcha 专项 Siamese 模型
|
||||
"""
|
||||
|
||||
226
training/data_fingerprint.py
Normal file
226
training/data_fingerprint.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
合成数据集指纹与清单辅助工具。
|
||||
|
||||
用于识别“样本数量足够但生成规则已变化”的情况,避免静默复用过期数据。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
MANIFEST_NAME = ".dataset_meta.json"
|
||||
|
||||
|
||||
def _stable_json(data: dict) -> str:
|
||||
return json.dumps(data, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def _sha256_text(text: str) -> str:
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _source_hash(obj) -> str:
|
||||
try:
|
||||
source = inspect.getsource(obj)
|
||||
except (OSError, TypeError):
|
||||
source = repr(obj)
|
||||
return _sha256_text(source)
|
||||
|
||||
|
||||
def dataset_manifest_path(dataset_dir: str | Path) -> Path:
|
||||
return Path(dataset_dir) / MANIFEST_NAME
|
||||
|
||||
|
||||
def dataset_spec_hash(spec: dict) -> str:
|
||||
return _sha256_text(_stable_json(spec))
|
||||
|
||||
|
||||
def build_dataset_spec(
|
||||
generator_cls,
|
||||
*,
|
||||
config_key: str,
|
||||
config_snapshot: dict,
|
||||
) -> dict:
|
||||
"""构造可稳定哈希的数据集规格说明。"""
|
||||
return {
|
||||
"config_key": config_key,
|
||||
"generator": f"{generator_cls.__module__}.{generator_cls.__name__}",
|
||||
"generator_source_hash": _source_hash(generator_cls),
|
||||
"config_snapshot": config_snapshot,
|
||||
}
|
||||
|
||||
|
||||
def load_dataset_manifest(dataset_dir: str | Path) -> dict | None:
|
||||
path = dataset_manifest_path(dataset_dir)
|
||||
if not path.exists():
|
||||
return None
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def write_dataset_manifest(
|
||||
dataset_dir: str | Path,
|
||||
*,
|
||||
spec: dict,
|
||||
sample_count: int,
|
||||
adopted_existing: bool,
|
||||
) -> dict:
|
||||
path = dataset_manifest_path(dataset_dir)
|
||||
manifest = {
|
||||
"version": 1,
|
||||
"spec": spec,
|
||||
"spec_hash": dataset_spec_hash(spec),
|
||||
"sample_count": sample_count,
|
||||
"adopted_existing": adopted_existing,
|
||||
}
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as f:
|
||||
json.dump(manifest, f, ensure_ascii=True, indent=2, sort_keys=True)
|
||||
f.write("\n")
|
||||
return manifest
|
||||
|
||||
|
||||
def labels_cover_tokens(files: list[Path], required_tokens: tuple[str, ...]) -> bool:
|
||||
"""检查文件名标签中是否至少覆盖每个目标 token 一次。"""
|
||||
remaining = set(required_tokens)
|
||||
if not remaining:
|
||||
return True
|
||||
|
||||
for path in files:
|
||||
label = path.stem.rsplit("_", 1)[0]
|
||||
matched = {token for token in remaining if token in label}
|
||||
if matched:
|
||||
remaining -= matched
|
||||
if not remaining:
|
||||
return True
|
||||
return not remaining
|
||||
|
||||
|
||||
def _count_matches(count: int, *, exact_count: int | None, min_count: int | None) -> bool:
|
||||
if exact_count is not None and count != exact_count:
|
||||
return False
|
||||
if min_count is not None and count < min_count:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _dataset_valid(
|
||||
files: list[Path],
|
||||
*,
|
||||
exact_count: int | None,
|
||||
min_count: int | None,
|
||||
validator: Callable[[list[Path]], bool] | None,
|
||||
) -> bool:
|
||||
counts_ok = _count_matches(len(files), exact_count=exact_count, min_count=min_count)
|
||||
if not counts_ok:
|
||||
return False
|
||||
if validator is None:
|
||||
return True
|
||||
return validator(files)
|
||||
|
||||
|
||||
def clear_generated_dataset(dataset_dir: str | Path) -> None:
|
||||
dataset_dir = Path(dataset_dir)
|
||||
for path in dataset_dir.glob("*.png"):
|
||||
path.unlink()
|
||||
manifest = dataset_manifest_path(dataset_dir)
|
||||
if manifest.exists():
|
||||
manifest.unlink()
|
||||
|
||||
|
||||
def ensure_synthetic_dataset(
|
||||
dataset_dir: str | Path,
|
||||
*,
|
||||
generator_cls,
|
||||
spec: dict,
|
||||
gen_count: int,
|
||||
exact_count: int | None = None,
|
||||
min_count: int | None = None,
|
||||
validator: Callable[[list[Path]], bool] | None = None,
|
||||
adopt_if_missing: bool = False,
|
||||
) -> dict:
|
||||
"""
|
||||
确保合成数据与当前生成规则一致。
|
||||
|
||||
返回:
|
||||
{
|
||||
"manifest": dict,
|
||||
"sample_count": int,
|
||||
"refreshed": bool,
|
||||
"adopted": bool,
|
||||
}
|
||||
"""
|
||||
dataset_dir = Path(dataset_dir)
|
||||
dataset_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
files = sorted(dataset_dir.glob("*.png"))
|
||||
sample_count = len(files)
|
||||
counts_ok = _count_matches(sample_count, exact_count=exact_count, min_count=min_count)
|
||||
validator_ok = _dataset_valid(
|
||||
files,
|
||||
exact_count=exact_count,
|
||||
min_count=min_count,
|
||||
validator=validator,
|
||||
)
|
||||
manifest = load_dataset_manifest(dataset_dir)
|
||||
spec_hash = dataset_spec_hash(spec)
|
||||
|
||||
manifest_ok = (
|
||||
manifest is not None
|
||||
and manifest.get("spec_hash") == spec_hash
|
||||
and manifest.get("sample_count") == sample_count
|
||||
)
|
||||
|
||||
if manifest_ok and counts_ok and validator_ok:
|
||||
return {
|
||||
"manifest": manifest,
|
||||
"sample_count": sample_count,
|
||||
"refreshed": False,
|
||||
"adopted": False,
|
||||
}
|
||||
|
||||
if manifest is None and adopt_if_missing and counts_ok and validator_ok:
|
||||
manifest = write_dataset_manifest(
|
||||
dataset_dir,
|
||||
spec=spec,
|
||||
sample_count=sample_count,
|
||||
adopted_existing=True,
|
||||
)
|
||||
return {
|
||||
"manifest": manifest,
|
||||
"sample_count": sample_count,
|
||||
"refreshed": False,
|
||||
"adopted": True,
|
||||
}
|
||||
|
||||
clear_generated_dataset(dataset_dir)
|
||||
gen = generator_cls()
|
||||
gen.generate_dataset(gen_count, str(dataset_dir))
|
||||
|
||||
files = sorted(dataset_dir.glob("*.png"))
|
||||
sample_count = len(files)
|
||||
if not _dataset_valid(
|
||||
files,
|
||||
exact_count=exact_count,
|
||||
min_count=min_count,
|
||||
validator=validator,
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"生成后的数据集不符合要求: {dataset_dir} (count={sample_count})"
|
||||
)
|
||||
manifest = write_dataset_manifest(
|
||||
dataset_dir,
|
||||
spec=spec,
|
||||
sample_count=sample_count,
|
||||
adopted_existing=False,
|
||||
)
|
||||
return {
|
||||
"manifest": manifest,
|
||||
"sample_count": sample_count,
|
||||
"refreshed": True,
|
||||
"adopted": False,
|
||||
}
|
||||
@@ -55,6 +55,33 @@ def build_val_transform(img_h: int, img_w: int) -> transforms.Compose:
|
||||
])
|
||||
|
||||
|
||||
def build_train_rgb_transform(img_h: int, img_w: int) -> transforms.Compose:
|
||||
"""RGB 模型训练时数据增强 transform。"""
|
||||
aug = AUGMENT_CONFIG
|
||||
return transforms.Compose([
|
||||
transforms.Resize((img_h, img_w)),
|
||||
transforms.RandomAffine(
|
||||
degrees=aug["degrees"],
|
||||
translate=aug["translate"],
|
||||
scale=aug["scale"],
|
||||
),
|
||||
transforms.ColorJitter(brightness=aug["brightness"], contrast=aug["contrast"]),
|
||||
transforms.GaussianBlur(aug["blur_kernel"], sigma=aug["blur_sigma"]),
|
||||
transforms.ToTensor(),
|
||||
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]),
|
||||
transforms.RandomErasing(p=aug["erasing_prob"], scale=aug["erasing_scale"]),
|
||||
])
|
||||
|
||||
|
||||
def build_val_rgb_transform(img_h: int, img_w: int) -> transforms.Compose:
|
||||
"""RGB 模型验证 / 推理时 transform。"""
|
||||
return transforms.Compose([
|
||||
transforms.Resize((img_h, img_w)),
|
||||
transforms.ToTensor(),
|
||||
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]),
|
||||
])
|
||||
|
||||
|
||||
# ============================================================
|
||||
# CRNN / CTC 识别用数据集
|
||||
# ============================================================
|
||||
@@ -276,3 +303,82 @@ class RotateSolverDataset(Dataset):
|
||||
img = self.transform(img)
|
||||
return img, torch.tensor([sin_val, cos_val], dtype=torch.float32)
|
||||
|
||||
|
||||
class FunCaptchaChallengeDataset(Dataset):
|
||||
"""
|
||||
FunCaptcha 专项 challenge 数据集。
|
||||
|
||||
输入为整张 challenge 图片,文件名标签表示正确候选索引:
|
||||
`{answer_index}_{anything}.png/jpg/jpeg`
|
||||
|
||||
每个样本会被裁成:
|
||||
- `candidates`: (K, C, H, W)
|
||||
- `reference`: (C, H, W)
|
||||
- `answer_idx`: LongTensor 标量
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dirs: list[str | Path],
|
||||
task_config: dict,
|
||||
transform: transforms.Compose | None = None,
|
||||
):
|
||||
import warnings
|
||||
|
||||
self.transform = transform
|
||||
self.tile_w, self.tile_h = task_config["tile_size"]
|
||||
self.reference_box = tuple(task_config["reference_box"])
|
||||
self.num_candidates = int(task_config["num_candidates"])
|
||||
self.answer_index_base = int(task_config.get("answer_index_base", 0))
|
||||
|
||||
self.samples: list[tuple[str, int]] = [] # (路径, 0-based answer_idx)
|
||||
for d in dirs:
|
||||
d = Path(d)
|
||||
if not d.exists():
|
||||
continue
|
||||
|
||||
for pattern in ("*.png", "*.jpg", "*.jpeg"):
|
||||
for f in sorted(d.glob(pattern)):
|
||||
raw_label = f.stem.rsplit("_", 1)[0]
|
||||
try:
|
||||
answer_idx = int(raw_label) - self.answer_index_base
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if not (0 <= answer_idx < self.num_candidates):
|
||||
warnings.warn(
|
||||
f"FunCaptcha 标签越界: file={f} label={raw_label} "
|
||||
f"expect=[{self.answer_index_base}, {self.answer_index_base + self.num_candidates - 1}]",
|
||||
stacklevel=2,
|
||||
)
|
||||
continue
|
||||
|
||||
self.samples.append((str(f), answer_idx))
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.samples)
|
||||
|
||||
def __getitem__(self, idx: int):
|
||||
import torch
|
||||
|
||||
path, answer_idx = self.samples[idx]
|
||||
image = Image.open(path).convert("RGB")
|
||||
|
||||
candidates = []
|
||||
for i in range(self.num_candidates):
|
||||
left = i * self.tile_w
|
||||
box = (left, 0, left + self.tile_w, self.tile_h)
|
||||
candidate = image.crop(box)
|
||||
if self.transform:
|
||||
candidate = self.transform(candidate)
|
||||
candidates.append(candidate)
|
||||
|
||||
reference = image.crop(self.reference_box)
|
||||
if self.transform:
|
||||
reference = self.transform(reference)
|
||||
|
||||
return (
|
||||
torch.stack(candidates, dim=0),
|
||||
reference,
|
||||
torch.tensor(answer_idx, dtype=torch.long),
|
||||
)
|
||||
|
||||
@@ -23,6 +23,11 @@ from config import (
|
||||
NUM_CAPTCHA_TYPES,
|
||||
IMAGE_SIZE,
|
||||
TRAIN_CONFIG,
|
||||
GENERATE_CONFIG,
|
||||
NORMAL_CHARS,
|
||||
MATH_CHARS,
|
||||
THREED_CHARS,
|
||||
REGRESSION_RANGE,
|
||||
CLASSIFIER_DIR,
|
||||
SYNTHETIC_NORMAL_DIR,
|
||||
SYNTHETIC_MATH_DIR,
|
||||
@@ -40,7 +45,13 @@ from generators.math_gen import MathCaptchaGenerator
|
||||
from generators.threed_gen import ThreeDCaptchaGenerator
|
||||
from generators.threed_rotate_gen import ThreeDRotateGenerator
|
||||
from generators.threed_slider_gen import ThreeDSliderGenerator
|
||||
from inference.model_metadata import write_model_metadata
|
||||
from models.classifier import CaptchaClassifier
|
||||
from training.data_fingerprint import (
|
||||
build_dataset_spec,
|
||||
ensure_synthetic_dataset,
|
||||
labels_cover_tokens,
|
||||
)
|
||||
from training.dataset import CaptchaDataset, build_train_transform, build_val_transform
|
||||
|
||||
|
||||
@@ -63,27 +74,55 @@ def _prepare_classifier_data():
|
||||
("3d_rotate", SYNTHETIC_3D_ROTATE_DIR, ThreeDRotateGenerator),
|
||||
("3d_slider", SYNTHETIC_3D_SLIDER_DIR, ThreeDSliderGenerator),
|
||||
]
|
||||
chars_map = {
|
||||
"normal": NORMAL_CHARS,
|
||||
"math": MATH_CHARS,
|
||||
"3d_text": THREED_CHARS,
|
||||
}
|
||||
|
||||
for cls_name, syn_dir, gen_cls in type_info:
|
||||
syn_dir = Path(syn_dir)
|
||||
existing = sorted(syn_dir.glob("*.png"))
|
||||
config_snapshot = {
|
||||
"generate_config": GENERATE_CONFIG[cls_name],
|
||||
"image_size": IMAGE_SIZE[cls_name],
|
||||
}
|
||||
if cls_name in chars_map:
|
||||
config_snapshot["chars"] = chars_map[cls_name]
|
||||
if cls_name in REGRESSION_RANGE:
|
||||
config_snapshot["label_range"] = REGRESSION_RANGE[cls_name]
|
||||
|
||||
# 如果合成数据不够,生成一些
|
||||
if len(existing) < per_class:
|
||||
print(f"[数据] {cls_name} 合成数据不足 ({len(existing)}/{per_class}),开始生成...")
|
||||
gen = gen_cls()
|
||||
gen.generate_dataset(per_class, str(syn_dir))
|
||||
existing = sorted(syn_dir.glob("*.png"))
|
||||
validator = None
|
||||
if cls_name == "math":
|
||||
required_ops = tuple(GENERATE_CONFIG["math"]["operators"])
|
||||
validator = lambda files, tokens=required_ops: labels_cover_tokens(files, tokens)
|
||||
|
||||
dataset_state = ensure_synthetic_dataset(
|
||||
syn_dir,
|
||||
generator_cls=gen_cls,
|
||||
spec=build_dataset_spec(
|
||||
gen_cls,
|
||||
config_key=cls_name,
|
||||
config_snapshot=config_snapshot,
|
||||
),
|
||||
gen_count=per_class,
|
||||
min_count=per_class,
|
||||
validator=validator,
|
||||
adopt_if_missing=cls_name in {"normal", "math"},
|
||||
)
|
||||
if dataset_state["refreshed"]:
|
||||
print(f"[数据] {cls_name} 合成数据已刷新: {dataset_state['sample_count']} 张")
|
||||
elif dataset_state["adopted"]:
|
||||
print(f"[数据] {cls_name} 合成数据已采纳并写入指纹: {dataset_state['sample_count']} 张")
|
||||
else:
|
||||
print(f"[数据] {cls_name} 合成数据已就绪: {dataset_state['sample_count']} 张")
|
||||
|
||||
existing = sorted(syn_dir.glob("*.png"))
|
||||
|
||||
# 复制到 classifier 目录
|
||||
cls_dir = CLASSIFIER_DIR / cls_name
|
||||
cls_dir.mkdir(parents=True, exist_ok=True)
|
||||
already = len(list(cls_dir.glob("*.png")))
|
||||
if already >= per_class:
|
||||
print(f"[数据] {cls_name} 分类器数据已就绪: {already} 张")
|
||||
continue
|
||||
|
||||
# 清空后重新链接
|
||||
# classifier 数据是派生目录,每次重建以对齐当前源数据与指纹状态。
|
||||
for f in cls_dir.glob("*.png"):
|
||||
f.unlink()
|
||||
|
||||
@@ -239,6 +278,15 @@ def main():
|
||||
if ONNX_CONFIG["dynamic_batch"]
|
||||
else None,
|
||||
)
|
||||
write_model_metadata(
|
||||
onnx_path,
|
||||
{
|
||||
"model_name": "classifier",
|
||||
"task": "classifier",
|
||||
"class_names": list(CAPTCHA_TYPES),
|
||||
"input_shape": [1, img_h, img_w],
|
||||
},
|
||||
)
|
||||
print(f"[ONNX] 导出完成: {onnx_path} ({onnx_path.stat().st_size / 1024:.1f} KB)")
|
||||
|
||||
return best_acc
|
||||
|
||||
248
training/train_funcaptcha_rollball.py
Normal file
248
training/train_funcaptcha_rollball.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
训练 FunCaptcha `4_3d_rollball_animals` 专项 Siamese 模型。
|
||||
|
||||
数据格式:
|
||||
data/real/funcaptcha/4_3d_rollball_animals/
|
||||
0_xxx.png
|
||||
1_xxx.jpg
|
||||
2_xxx.jpeg
|
||||
|
||||
文件名前缀表示正确候选索引。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.utils.data import DataLoader, random_split
|
||||
from tqdm import tqdm
|
||||
|
||||
from config import (
|
||||
CHECKPOINTS_DIR,
|
||||
FUN_CAPTCHA_TASKS,
|
||||
IMAGE_SIZE,
|
||||
RANDOM_SEED,
|
||||
TRAIN_CONFIG,
|
||||
get_device,
|
||||
)
|
||||
from inference.export_onnx import _load_and_export
|
||||
from models.fun_captcha_siamese import FunCaptchaSiamese
|
||||
from training.dataset import (
|
||||
FunCaptchaChallengeDataset,
|
||||
build_train_rgb_transform,
|
||||
build_val_rgb_transform,
|
||||
)
|
||||
|
||||
|
||||
QUESTION = "4_3d_rollball_animals"
|
||||
|
||||
|
||||
def _set_seed():
|
||||
random.seed(RANDOM_SEED)
|
||||
np.random.seed(RANDOM_SEED)
|
||||
torch.manual_seed(RANDOM_SEED)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed_all(RANDOM_SEED)
|
||||
|
||||
|
||||
def _flatten_pairs(
|
||||
candidates: torch.Tensor,
|
||||
reference: torch.Tensor,
|
||||
answer_idx: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
batch_size, num_candidates, channels, img_h, img_w = candidates.shape
|
||||
references = reference.unsqueeze(1).expand(-1, num_candidates, -1, -1, -1)
|
||||
targets = F.one_hot(answer_idx, num_classes=num_candidates).float()
|
||||
return (
|
||||
candidates.reshape(batch_size * num_candidates, channels, img_h, img_w),
|
||||
references.reshape(batch_size * num_candidates, channels, img_h, img_w),
|
||||
targets.reshape(batch_size * num_candidates, 1),
|
||||
)
|
||||
|
||||
|
||||
def _evaluate(
|
||||
model: FunCaptchaSiamese,
|
||||
loader: DataLoader,
|
||||
device: torch.device,
|
||||
) -> tuple[float, float]:
|
||||
model.eval()
|
||||
challenge_correct = 0
|
||||
challenge_total = 0
|
||||
pair_correct = 0
|
||||
pair_total = 0
|
||||
|
||||
with torch.no_grad():
|
||||
for candidates, reference, answer_idx in loader:
|
||||
candidates = candidates.to(device)
|
||||
reference = reference.to(device)
|
||||
answer_idx = answer_idx.to(device)
|
||||
|
||||
pair_candidates, pair_reference, pair_targets = _flatten_pairs(
|
||||
candidates, reference, answer_idx
|
||||
)
|
||||
logits = model(pair_candidates, pair_reference).view(candidates.size(0), candidates.size(1))
|
||||
preds = logits.argmax(dim=1)
|
||||
challenge_correct += (preds == answer_idx).sum().item()
|
||||
challenge_total += answer_idx.size(0)
|
||||
|
||||
pair_probs = torch.sigmoid(logits)
|
||||
pair_preds = (pair_probs >= 0.5).float()
|
||||
target_matrix = pair_targets.view(candidates.size(0), candidates.size(1))
|
||||
pair_correct += (pair_preds == target_matrix).sum().item()
|
||||
pair_total += target_matrix.numel()
|
||||
|
||||
return (
|
||||
challenge_correct / max(challenge_total, 1),
|
||||
pair_correct / max(pair_total, 1),
|
||||
)
|
||||
|
||||
|
||||
def main(question: str = QUESTION):
|
||||
task_cfg = FUN_CAPTCHA_TASKS[question]
|
||||
cfg = TRAIN_CONFIG["funcaptcha_rollball_animals"]
|
||||
img_h, img_w = IMAGE_SIZE["funcaptcha_rollball_animals"]
|
||||
device = get_device()
|
||||
data_dir = Path(task_cfg["data_dir"])
|
||||
ckpt_name = task_cfg["checkpoint_name"]
|
||||
ckpt_path = CHECKPOINTS_DIR / f"{ckpt_name}.pth"
|
||||
|
||||
_set_seed()
|
||||
|
||||
print("=" * 60)
|
||||
print(f"训练 FunCaptcha 专项模型 ({question})")
|
||||
print(f" 数据目录: {data_dir}")
|
||||
print(f" 候选数: {task_cfg['num_candidates']}")
|
||||
print(f" 输入尺寸: {img_h}×{img_w}")
|
||||
print("=" * 60)
|
||||
|
||||
train_transform = build_train_rgb_transform(img_h, img_w)
|
||||
val_transform = build_val_rgb_transform(img_h, img_w)
|
||||
|
||||
full_dataset = FunCaptchaChallengeDataset(
|
||||
dirs=[data_dir],
|
||||
task_config=task_cfg,
|
||||
transform=train_transform,
|
||||
)
|
||||
total = len(full_dataset)
|
||||
if total == 0:
|
||||
raise FileNotFoundError(
|
||||
f"未找到任何 FunCaptcha 训练样本,请先准备数据: {data_dir}"
|
||||
)
|
||||
|
||||
val_size = max(1, int(total * cfg["val_split"]))
|
||||
train_size = total - val_size
|
||||
if train_size <= 0:
|
||||
raise ValueError(f"FunCaptcha 数据量过少,至少需要 2 张样本: {data_dir}")
|
||||
|
||||
train_ds, val_ds = random_split(full_dataset, [train_size, val_size])
|
||||
val_ds_clean = FunCaptchaChallengeDataset(
|
||||
dirs=[data_dir],
|
||||
task_config=task_cfg,
|
||||
transform=val_transform,
|
||||
)
|
||||
val_ds_clean.samples = [full_dataset.samples[i] for i in val_ds.indices]
|
||||
|
||||
train_loader = DataLoader(
|
||||
train_ds,
|
||||
batch_size=cfg["batch_size"],
|
||||
shuffle=True,
|
||||
num_workers=0,
|
||||
pin_memory=True,
|
||||
)
|
||||
val_loader = DataLoader(
|
||||
val_ds_clean,
|
||||
batch_size=cfg["batch_size"],
|
||||
shuffle=False,
|
||||
num_workers=0,
|
||||
pin_memory=True,
|
||||
)
|
||||
|
||||
print(f"[数据] 训练: {train_size} 验证: {val_size}")
|
||||
|
||||
model = FunCaptchaSiamese(in_channels=task_cfg["channels"]).to(device)
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=cfg["lr"])
|
||||
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=cfg["epochs"])
|
||||
pos_weight = torch.tensor([task_cfg["num_candidates"] - 1], dtype=torch.float32, device=device)
|
||||
criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)
|
||||
|
||||
best_acc = 0.0
|
||||
start_epoch = 1
|
||||
|
||||
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 = float(ckpt.get("best_acc", 0.0))
|
||||
start_epoch = int(ckpt.get("epoch", 0)) + 1
|
||||
for _ in range(start_epoch - 1):
|
||||
scheduler.step()
|
||||
print(f"[续训] 从 epoch {start_epoch} 继续, best_acc={best_acc:.4f}")
|
||||
|
||||
for epoch in range(start_epoch, cfg["epochs"] + 1):
|
||||
model.train()
|
||||
total_loss = 0.0
|
||||
num_batches = 0
|
||||
|
||||
pbar = tqdm(train_loader, desc=f"Epoch {epoch}/{cfg['epochs']}", leave=False)
|
||||
for candidates, reference, answer_idx in pbar:
|
||||
candidates = candidates.to(device)
|
||||
reference = reference.to(device)
|
||||
answer_idx = answer_idx.to(device)
|
||||
|
||||
pair_candidates, pair_reference, pair_targets = _flatten_pairs(
|
||||
candidates, reference, answer_idx
|
||||
)
|
||||
logits = model(pair_candidates, pair_reference)
|
||||
loss = criterion(logits, pair_targets)
|
||||
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0)
|
||||
optimizer.step()
|
||||
|
||||
total_loss += loss.item()
|
||||
num_batches += 1
|
||||
pbar.set_postfix(loss=f"{loss.item():.4f}")
|
||||
|
||||
scheduler.step()
|
||||
avg_loss = total_loss / max(num_batches, 1)
|
||||
challenge_acc, pair_acc = _evaluate(model, val_loader, device)
|
||||
lr = scheduler.get_last_lr()[0]
|
||||
|
||||
print(
|
||||
f"Epoch {epoch:3d}/{cfg['epochs']} "
|
||||
f"loss={avg_loss:.4f} "
|
||||
f"acc={challenge_acc:.4f} "
|
||||
f"pair_acc={pair_acc:.4f} "
|
||||
f"lr={lr:.6f}"
|
||||
)
|
||||
|
||||
if challenge_acc >= best_acc:
|
||||
best_acc = challenge_acc
|
||||
torch.save(
|
||||
{
|
||||
"model_state_dict": model.state_dict(),
|
||||
"best_acc": best_acc,
|
||||
"epoch": epoch,
|
||||
"question": question,
|
||||
"num_candidates": task_cfg["num_candidates"],
|
||||
"tile_size": list(task_cfg["tile_size"]),
|
||||
"reference_box": list(task_cfg["reference_box"]),
|
||||
"answer_index_base": task_cfg["answer_index_base"],
|
||||
"input_shape": [task_cfg["channels"], img_h, img_w],
|
||||
},
|
||||
ckpt_path,
|
||||
)
|
||||
print(f" → 保存最佳模型 acc={best_acc:.4f} {ckpt_path}")
|
||||
|
||||
print(f"\n[训练完成] 最佳 challenge acc: {best_acc:.4f}")
|
||||
_load_and_export(task_cfg["artifact_name"])
|
||||
return best_acc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -26,11 +26,16 @@ from config import (
|
||||
ONNX_CONFIG,
|
||||
TRAIN_CONFIG,
|
||||
IMAGE_SIZE,
|
||||
GENERATE_CONFIG,
|
||||
REGRESSION_RANGE,
|
||||
SOLVER_CONFIG,
|
||||
SOLVER_REGRESSION_RANGE,
|
||||
RANDOM_SEED,
|
||||
get_device,
|
||||
)
|
||||
from inference.model_metadata import write_model_metadata
|
||||
from training.dataset import RegressionDataset, build_train_transform, build_val_transform
|
||||
from training.data_fingerprint import build_dataset_spec, ensure_synthetic_dataset
|
||||
|
||||
|
||||
def _set_seed(seed: int = RANDOM_SEED):
|
||||
@@ -65,7 +70,14 @@ def _circular_mae(pred: np.ndarray, target: np.ndarray) -> float:
|
||||
return float(np.mean(diff))
|
||||
|
||||
|
||||
def _export_onnx(model: nn.Module, model_name: str, img_h: int, img_w: int):
|
||||
def _export_onnx(
|
||||
model: nn.Module,
|
||||
model_name: str,
|
||||
img_h: int,
|
||||
img_w: int,
|
||||
*,
|
||||
label_range: tuple[int, int] | tuple[float, float],
|
||||
):
|
||||
"""导出模型为 ONNX 格式。"""
|
||||
model.eval()
|
||||
onnx_path = ONNX_DIR / f"{model_name}.onnx"
|
||||
@@ -81,6 +93,15 @@ def _export_onnx(model: nn.Module, model_name: str, img_h: int, img_w: int):
|
||||
if ONNX_CONFIG["dynamic_batch"]
|
||||
else None,
|
||||
)
|
||||
write_model_metadata(
|
||||
onnx_path,
|
||||
{
|
||||
"model_name": model_name,
|
||||
"task": "regression",
|
||||
"label_range": list(label_range),
|
||||
"input_shape": [1, img_h, img_w],
|
||||
},
|
||||
)
|
||||
print(f"[ONNX] 导出完成: {onnx_path} ({onnx_path.stat().st_size / 1024:.1f} KB)")
|
||||
|
||||
|
||||
@@ -120,13 +141,37 @@ def train_regression_model(
|
||||
|
||||
# ---- 1. 检查 / 生成合成数据 ----
|
||||
syn_path = Path(synthetic_dir)
|
||||
existing = list(syn_path.glob("*.png"))
|
||||
if len(existing) < cfg["synthetic_samples"]:
|
||||
print(f"[数据] 合成数据不足 ({len(existing)}/{cfg['synthetic_samples']}),开始生成...")
|
||||
gen = generator_cls()
|
||||
gen.generate_dataset(cfg["synthetic_samples"], str(syn_path))
|
||||
config_snapshot = {
|
||||
"image_size": IMAGE_SIZE[config_key],
|
||||
"label_range": label_range,
|
||||
}
|
||||
if config_key in GENERATE_CONFIG:
|
||||
config_snapshot["generate_config"] = GENERATE_CONFIG[config_key]
|
||||
elif config_key == "slide_cnn":
|
||||
config_snapshot["solver_config"] = SOLVER_CONFIG["slide"]
|
||||
config_snapshot["solver_regression_range"] = SOLVER_REGRESSION_RANGE["slide"]
|
||||
else:
|
||||
print(f"[数据] 合成数据已就绪: {len(existing)} 张")
|
||||
config_snapshot["train_config"] = cfg
|
||||
|
||||
dataset_spec = build_dataset_spec(
|
||||
generator_cls,
|
||||
config_key=config_key,
|
||||
config_snapshot=config_snapshot,
|
||||
)
|
||||
dataset_state = ensure_synthetic_dataset(
|
||||
syn_path,
|
||||
generator_cls=generator_cls,
|
||||
spec=dataset_spec,
|
||||
gen_count=cfg["synthetic_samples"],
|
||||
exact_count=cfg["synthetic_samples"],
|
||||
)
|
||||
if dataset_state["refreshed"]:
|
||||
print(f"[数据] 合成数据已刷新: {dataset_state['sample_count']} 张")
|
||||
elif dataset_state["adopted"]:
|
||||
print(f"[数据] 现有合成数据已采纳并写入指纹: {dataset_state['sample_count']} 张")
|
||||
else:
|
||||
print(f"[数据] 合成数据已就绪: {dataset_state['sample_count']} 张")
|
||||
current_data_spec_hash = dataset_state["manifest"]["spec_hash"]
|
||||
|
||||
# ---- 2. 构建数据集 ----
|
||||
data_dirs = [str(syn_path)]
|
||||
@@ -181,17 +226,26 @@ def train_regression_model(
|
||||
# ---- 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}"
|
||||
)
|
||||
ckpt_data_spec_hash = ckpt.get("synthetic_data_spec_hash")
|
||||
|
||||
if dataset_state["refreshed"]:
|
||||
print("[续训] 合成数据已刷新,忽略旧 checkpoint,从 epoch 1 重新训练")
|
||||
elif ckpt_data_spec_hash is not None and ckpt_data_spec_hash != current_data_spec_hash:
|
||||
print("[续训] checkpoint 与当前合成数据指纹不一致,从 epoch 1 重新训练")
|
||||
else:
|
||||
if ckpt_data_spec_hash is None:
|
||||
print("[续训] 旧 checkpoint 缺少数据指纹,沿用现有权重继续训练")
|
||||
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(start_epoch, cfg["epochs"] + 1):
|
||||
@@ -268,6 +322,7 @@ def train_regression_model(
|
||||
"best_mae": best_mae,
|
||||
"best_tol_acc": best_tol_acc,
|
||||
"epoch": epoch,
|
||||
"synthetic_data_spec_hash": current_data_spec_hash,
|
||||
}, ckpt_path)
|
||||
print(f" → 保存最佳模型 tol_acc={best_tol_acc:.4f} MAE={best_mae:.2f} {ckpt_path}")
|
||||
|
||||
@@ -275,6 +330,6 @@ def train_regression_model(
|
||||
print(f"\n[训练完成] 最佳容差准确率: {best_tol_acc:.4f} 最佳 MAE: {best_mae:.2f}")
|
||||
ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=True)
|
||||
model.load_state_dict(ckpt["model_state_dict"])
|
||||
_export_onnx(model, model_name, img_h, img_w)
|
||||
_export_onnx(model, model_name, img_h, img_w, label_range=label_range)
|
||||
|
||||
return best_tol_acc
|
||||
|
||||
@@ -29,7 +29,9 @@ from config import (
|
||||
get_device,
|
||||
)
|
||||
from generators.rotate_solver_gen import RotateSolverDataGenerator
|
||||
from inference.model_metadata import write_model_metadata
|
||||
from models.rotation_regressor import RotationRegressor
|
||||
from training.data_fingerprint import build_dataset_spec, ensure_synthetic_dataset
|
||||
from training.dataset import RotateSolverDataset
|
||||
|
||||
|
||||
@@ -85,6 +87,15 @@ def _export_onnx(model: nn.Module, img_h: int, img_w: int):
|
||||
if ONNX_CONFIG["dynamic_batch"]
|
||||
else None,
|
||||
)
|
||||
write_model_metadata(
|
||||
onnx_path,
|
||||
{
|
||||
"model_name": "rotation_regressor",
|
||||
"task": "rotation_solver",
|
||||
"output_encoding": "sin_cos",
|
||||
"input_shape": [3, img_h, img_w],
|
||||
},
|
||||
)
|
||||
print(f"[ONNX] 导出完成: {onnx_path} ({onnx_path.stat().st_size / 1024:.1f} KB)")
|
||||
|
||||
|
||||
@@ -107,13 +118,30 @@ def main():
|
||||
# ---- 1. 检查 / 生成合成数据 ----
|
||||
syn_path = ROTATE_SOLVER_DATA_DIR
|
||||
syn_path.mkdir(parents=True, exist_ok=True)
|
||||
existing = list(syn_path.glob("*.png"))
|
||||
if len(existing) < cfg["synthetic_samples"]:
|
||||
print(f"[数据] 合成数据不足 ({len(existing)}/{cfg['synthetic_samples']}),开始生成...")
|
||||
gen = RotateSolverDataGenerator()
|
||||
gen.generate_dataset(cfg["synthetic_samples"], str(syn_path))
|
||||
dataset_spec = build_dataset_spec(
|
||||
RotateSolverDataGenerator,
|
||||
config_key="rotate_solver",
|
||||
config_snapshot={
|
||||
"solver_config": SOLVER_CONFIG["rotate"],
|
||||
"train_config": {
|
||||
"synthetic_samples": cfg["synthetic_samples"],
|
||||
},
|
||||
},
|
||||
)
|
||||
dataset_state = ensure_synthetic_dataset(
|
||||
syn_path,
|
||||
generator_cls=RotateSolverDataGenerator,
|
||||
spec=dataset_spec,
|
||||
gen_count=cfg["synthetic_samples"],
|
||||
exact_count=cfg["synthetic_samples"],
|
||||
)
|
||||
if dataset_state["refreshed"]:
|
||||
print(f"[数据] 合成数据已刷新: {dataset_state['sample_count']} 张")
|
||||
elif dataset_state["adopted"]:
|
||||
print(f"[数据] 现有合成数据已采纳并写入指纹: {dataset_state['sample_count']} 张")
|
||||
else:
|
||||
print(f"[数据] 合成数据已就绪: {len(existing)} 张")
|
||||
print(f"[数据] 合成数据已就绪: {dataset_state['sample_count']} 张")
|
||||
current_data_spec_hash = dataset_state["manifest"]["spec_hash"]
|
||||
|
||||
# ---- 2. 构建数据集 ----
|
||||
data_dirs = [str(syn_path)]
|
||||
@@ -229,6 +257,7 @@ def main():
|
||||
"best_mae": best_mae,
|
||||
"best_tol_acc": best_tol_acc,
|
||||
"epoch": epoch,
|
||||
"synthetic_data_spec_hash": current_data_spec_hash,
|
||||
}, ckpt_path)
|
||||
print(f" → 保存最佳模型 tol_acc={best_tol_acc:.4f} MAE={best_mae:.2f}° {ckpt_path}")
|
||||
|
||||
|
||||
@@ -27,9 +27,16 @@ from config import (
|
||||
ONNX_CONFIG,
|
||||
TRAIN_CONFIG,
|
||||
IMAGE_SIZE,
|
||||
GENERATE_CONFIG,
|
||||
RANDOM_SEED,
|
||||
get_device,
|
||||
)
|
||||
from inference.model_metadata import write_model_metadata
|
||||
from training.data_fingerprint import (
|
||||
build_dataset_spec,
|
||||
ensure_synthetic_dataset,
|
||||
labels_cover_tokens,
|
||||
)
|
||||
from training.dataset import CRNNDataset, build_train_transform, build_val_transform
|
||||
|
||||
|
||||
@@ -72,7 +79,14 @@ def _calc_accuracy(preds: list[str], labels: list[str]):
|
||||
# ============================================================
|
||||
# ONNX 导出
|
||||
# ============================================================
|
||||
def _export_onnx(model: nn.Module, model_name: str, img_h: int, img_w: int):
|
||||
def _export_onnx(
|
||||
model: nn.Module,
|
||||
model_name: str,
|
||||
img_h: int,
|
||||
img_w: int,
|
||||
*,
|
||||
chars: str,
|
||||
):
|
||||
"""导出模型为 ONNX 格式。"""
|
||||
model.eval()
|
||||
onnx_path = ONNX_DIR / f"{model_name}.onnx"
|
||||
@@ -88,6 +102,15 @@ def _export_onnx(model: nn.Module, model_name: str, img_h: int, img_w: int):
|
||||
if ONNX_CONFIG["dynamic_batch"]
|
||||
else None,
|
||||
)
|
||||
write_model_metadata(
|
||||
onnx_path,
|
||||
{
|
||||
"model_name": model_name,
|
||||
"task": "ctc",
|
||||
"chars": chars,
|
||||
"input_shape": [1, img_h, img_w],
|
||||
},
|
||||
)
|
||||
print(f"[ONNX] 导出完成: {onnx_path} ({onnx_path.stat().st_size / 1024:.1f} KB)")
|
||||
|
||||
|
||||
@@ -124,13 +147,36 @@ def train_ctc_model(
|
||||
|
||||
# ---- 1. 检查 / 生成合成数据 ----
|
||||
syn_path = Path(synthetic_dir)
|
||||
existing = list(syn_path.glob("*.png"))
|
||||
if len(existing) < cfg["synthetic_samples"]:
|
||||
print(f"[数据] 合成数据不足 ({len(existing)}/{cfg['synthetic_samples']}),开始生成...")
|
||||
gen = generator_cls()
|
||||
gen.generate_dataset(cfg["synthetic_samples"], str(syn_path))
|
||||
dataset_spec = build_dataset_spec(
|
||||
generator_cls,
|
||||
config_key=config_key,
|
||||
config_snapshot={
|
||||
"generate_config": GENERATE_CONFIG[config_key],
|
||||
"chars": chars,
|
||||
"image_size": IMAGE_SIZE[config_key],
|
||||
},
|
||||
)
|
||||
validator = None
|
||||
if config_key == "math":
|
||||
required_ops = tuple(GENERATE_CONFIG["math"]["operators"])
|
||||
validator = lambda files: labels_cover_tokens(files, required_ops)
|
||||
|
||||
dataset_state = ensure_synthetic_dataset(
|
||||
syn_path,
|
||||
generator_cls=generator_cls,
|
||||
spec=dataset_spec,
|
||||
gen_count=cfg["synthetic_samples"],
|
||||
exact_count=cfg["synthetic_samples"],
|
||||
validator=validator,
|
||||
adopt_if_missing=config_key in {"normal", "math"},
|
||||
)
|
||||
if dataset_state["refreshed"]:
|
||||
print(f"[数据] 合成数据已刷新: {dataset_state['sample_count']} 张")
|
||||
elif dataset_state["adopted"]:
|
||||
print(f"[数据] 现有合成数据已采纳并写入指纹: {dataset_state['sample_count']} 张")
|
||||
else:
|
||||
print(f"[数据] 合成数据已就绪: {len(existing)} 张")
|
||||
print(f"[数据] 合成数据已就绪: {dataset_state['sample_count']} 张")
|
||||
current_data_spec_hash = dataset_state["manifest"]["spec_hash"]
|
||||
|
||||
# ---- 2. 构建数据集 ----
|
||||
data_dirs = [str(syn_path)]
|
||||
@@ -176,16 +222,25 @@ def train_ctc_model(
|
||||
# ---- 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}"
|
||||
)
|
||||
ckpt_data_spec_hash = ckpt.get("synthetic_data_spec_hash")
|
||||
|
||||
if dataset_state["refreshed"]:
|
||||
print("[续训] 合成数据已刷新,忽略旧 checkpoint,从 epoch 1 重新训练")
|
||||
elif ckpt_data_spec_hash is not None and ckpt_data_spec_hash != current_data_spec_hash:
|
||||
print("[续训] checkpoint 与当前合成数据指纹不一致,从 epoch 1 重新训练")
|
||||
else:
|
||||
if ckpt_data_spec_hash is None:
|
||||
print("[续训] 旧 checkpoint 缺少数据指纹,沿用现有权重继续训练")
|
||||
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(start_epoch, cfg["epochs"] + 1):
|
||||
@@ -249,6 +304,7 @@ def train_ctc_model(
|
||||
"chars": chars,
|
||||
"best_acc": best_acc,
|
||||
"epoch": epoch,
|
||||
"synthetic_data_spec_hash": current_data_spec_hash,
|
||||
}, ckpt_path)
|
||||
print(f" → 保存最佳模型 acc={best_acc:.4f} {ckpt_path}")
|
||||
|
||||
@@ -257,6 +313,6 @@ def train_ctc_model(
|
||||
# 加载最佳权重再导出
|
||||
ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=True)
|
||||
model.load_state_dict(ckpt["model_state_dict"])
|
||||
_export_onnx(model, model_name, img_h, img_w)
|
||||
_export_onnx(model, model_name, img_h, img_w, chars=chars)
|
||||
|
||||
return best_acc
|
||||
|
||||
663
uv.lock
generated
663
uv.lock
generated
@@ -1,11 +1,13 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.10, <3.13"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13'",
|
||||
"python_full_version == '3.12.*'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version < '3.11'",
|
||||
"python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'linux')",
|
||||
"(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'linux')",
|
||||
"(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'linux')",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -33,7 +35,7 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
|
||||
wheels = [
|
||||
@@ -43,7 +45,7 @@ wheels = [
|
||||
[[package]]
|
||||
name = "captchbreaker"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
source = { editable = "." }
|
||||
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'" },
|
||||
@@ -51,8 +53,10 @@ dependencies = [
|
||||
{ name = "onnxruntime" },
|
||||
{ name = "onnxscript" },
|
||||
{ name = "pillow" },
|
||||
{ name = "torch" },
|
||||
{ name = "torchvision" },
|
||||
{ name = "torch", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
|
||||
{ name = "torch", version = "2.5.1+cu121", source = { registry = "https://download.pytorch.org/whl/cu121" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "torchvision", version = "0.20.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
|
||||
{ name = "torchvision", version = "0.20.1+cu121", source = { registry = "https://download.pytorch.org/whl/cu121" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
|
||||
@@ -74,14 +78,16 @@ requires-dist = [
|
||||
{ name = "fastapi", marker = "extra == 'server'", specifier = ">=0.100.0" },
|
||||
{ name = "numpy", specifier = ">=1.24.0" },
|
||||
{ name = "onnx", specifier = ">=1.14.0" },
|
||||
{ name = "onnxruntime", specifier = ">=1.15.0" },
|
||||
{ name = "onnxruntime", specifier = ">=1.15.0,<1.24.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 = "torch", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'", specifier = "==2.5.1" },
|
||||
{ name = "torch", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = "==2.5.1", index = "https://download.pytorch.org/whl/cu121" },
|
||||
{ name = "torchvision", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'", specifier = "==0.20.1" },
|
||||
{ name = "torchvision", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = "==0.20.1", index = "https://download.pytorch.org/whl/cu121" },
|
||||
{ name = "tqdm", specifier = ">=4.65.0" },
|
||||
{ name = "uvicorn", marker = "extra == 'server'", specifier = ">=0.23.0" },
|
||||
]
|
||||
@@ -109,28 +115,15 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cuda-bindings"
|
||||
version = "12.9.4"
|
||||
name = "coloredlogs"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cuda-pathfinder" },
|
||||
{ name = "humanfriendly" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/d8/b546104b8da3f562c1ff8ab36d130c8fe1dd6a045ced80b4f6ad74f7d4e1/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5", size = 12148218, upload-time = "2025-10-21T14:51:28.855Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cuda-pathfinder"
|
||||
version = "1.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/02/59a5bc738a09def0b49aea0e460bdf97f65206d0d041246147cf6207e69c/cuda_pathfinder-1.4.1-py3-none-any.whl", hash = "sha256:40793006082de88e0950753655e55558a446bed9a7d9d0bcb48b2506d50ed82a", size = 43903, upload-time = "2026-03-06T21:05:24.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -196,6 +189,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humanfriendly"
|
||||
version = "10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
@@ -265,50 +270,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -335,26 +296,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -371,7 +312,8 @@ name = "networkx"
|
||||
version = "3.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
"python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'linux')",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" }
|
||||
wheels = [
|
||||
@@ -383,9 +325,10 @@ name = "networkx"
|
||||
version = "3.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13'",
|
||||
"python_full_version == '3.12.*'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'linux')",
|
||||
"(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'linux')",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
|
||||
wheels = [
|
||||
@@ -397,7 +340,8 @@ name = "numpy"
|
||||
version = "2.2.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
"python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'linux')",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
|
||||
wheels = [
|
||||
@@ -431,26 +375,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
|
||||
@@ -462,9 +386,10 @@ name = "numpy"
|
||||
version = "2.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13'",
|
||||
"python_full_version == '3.12.*'",
|
||||
"python_full_version == '3.11.*'",
|
||||
"python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'linux')",
|
||||
"(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'linux')",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" }
|
||||
wheels = [
|
||||
@@ -490,48 +415,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" },
|
||||
@@ -543,112 +426,93 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cublas-cu12"
|
||||
version = "12.8.4.1"
|
||||
version = "12.1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/6d/121efd7382d5b0284239f4ab1fc1590d86d34ed4a4a2fdb13b30ca8e5740/nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728", size = 410594774, upload-time = "2023-04-19T15:50:03.519Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cuda-cupti-cu12"
|
||||
version = "12.8.90"
|
||||
version = "12.1.105"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/00/6b218edd739ecfc60524e585ba8e6b00554dd908de2c9c66c1af3e44e18d/nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e", size = 14109015, upload-time = "2023-04-19T15:47:32.502Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cuda-nvrtc-cu12"
|
||||
version = "12.8.93"
|
||||
version = "12.1.105"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/9f/c64c03f49d6fbc56196664d05dba14e3a561038a81a638eeb47f4d4cfd48/nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2", size = 23671734, upload-time = "2023-04-19T15:48:32.42Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cuda-runtime-cu12"
|
||||
version = "12.8.90"
|
||||
version = "12.1.105"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/d5/c68b1d2cdfcc59e72e8a5949a37ddb22ae6cade80cd4a57a84d4c8b55472/nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40", size = 823596, upload-time = "2023-04-19T15:47:22.471Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cudnn-cu12"
|
||||
version = "9.10.2.21"
|
||||
version = "9.1.0.70"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas-cu12" },
|
||||
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741, upload-time = "2024-04-22T15:24:15.253Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cufft-cu12"
|
||||
version = "11.3.3.83"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink-cu12" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cufile-cu12"
|
||||
version = "1.13.1.3"
|
||||
version = "11.0.2.54"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/94/eb540db023ce1d162e7bea9f8f5aa781d57c65aed513c33ee9a5123ead4d/nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56", size = 121635161, upload-time = "2023-04-19T15:50:46Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-curand-cu12"
|
||||
version = "10.3.9.90"
|
||||
version = "10.3.2.106"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/31/4890b1c9abc496303412947fc7dcea3d14861720642b49e8ceed89636705/nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0", size = 56467784, upload-time = "2023-04-19T15:51:04.804Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cusolver-cu12"
|
||||
version = "11.7.3.90"
|
||||
version = "11.4.5.107"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas-cu12" },
|
||||
{ name = "nvidia-cusparse-cu12" },
|
||||
{ name = "nvidia-nvjitlink-cu12" },
|
||||
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928, upload-time = "2023-04-19T15:51:25.781Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cusparse-cu12"
|
||||
version = "12.5.8.93"
|
||||
version = "12.1.0.106"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink-cu12" },
|
||||
{ name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cusparselt-cu12"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278, upload-time = "2023-04-19T15:51:49.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-nccl-cu12"
|
||||
version = "2.27.5"
|
||||
version = "2.21.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/99/12cd266d6233f47d00daf3a72739872bdc10267d0383508b0b9c84a18bb6/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0", size = 188654414, upload-time = "2024-04-03T15:32:57.427Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -659,20 +523,12 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-nvshmem-cu12"
|
||||
version = "3.4.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-nvtx-cu12"
|
||||
version = "12.8.90"
|
||||
version = "12.1.105"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/d3/8057f0587683ed2fcd4dbfbdfdfa807b9160b809976099d36b8f60d08f03/nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5", size = 99138, upload-time = "2023-04-19T15:48:43.556Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -705,11 +561,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/74/a7/edce1403e05a46e59b502fae8e3350ceeac5841f8e8f1561e98562ed9b09/onnx-1.20.1-cp312-abi3-win32.whl", hash = "sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431", size = 16238216, upload-time = "2026-01-10T01:39:39.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/c7/8690c81200ae652ac550c1df52f89d7795e6cc941f3cb38c9ef821419e80/onnx-1.20.1-cp312-abi3-win_amd64.whl", hash = "sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5", size = 16389207, upload-time = "2026-01-10T01:39:41.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/a0/4fb0e6d36eaf079af366b2c1f68bafe92df6db963e2295da84388af64abc/onnx-1.20.1-cp312-abi3-win_arm64.whl", hash = "sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00", size = 16344155, upload-time = "2026-01-10T01:39:45.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/bb/715fad292b255664f0e603f1b2ef7bf2b386281775f37406beb99fa05957/onnx-1.20.1-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:29197b768f5acdd1568ddeb0a376407a2817844f6ac1ef8c8dd2d974c9ab27c3", size = 17912296, upload-time = "2026-01-10T01:39:48.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c3/541af12c3d45e159a94ee701100ba9e94b7bd8b7a8ac5ca6838569f894f8/onnx-1.20.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f0371aa67f51917a09cc829ada0f9a79a58f833449e03d748f7f7f53787c43c", size = 17416925, upload-time = "2026-01-10T01:39:50.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/3b/d5660a7d2ddf14f531ca66d409239f543bb290277c3f14f4b4b78e32efa3/onnx-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be1e5522200b203b34327b2cf132ddec20ab063469476e1f5b02bb7bd259a489", size = 17515602, upload-time = "2026-01-10T01:39:54.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/b4/47225ab2a92562eff87ba9a1a028e3535d659a7157d7cde659003998b8e3/onnx-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:15c815313bbc4b2fdc7e4daeb6e26b6012012adc4d850f4e3b09ed327a7ea92a", size = 16395729, upload-time = "2026-01-10T01:39:57.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/7d/1bbe626ff6b192c844d3ad34356840cc60fca02e2dea0db95e01645758b1/onnx-1.20.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eb335d7bcf9abac82a0d6a0fda0363531ae0b22cfd0fc6304bff32ee29905def", size = 16348968, upload-time = "2026-01-10T01:40:00.491Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -731,9 +582,10 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "onnxruntime"
|
||||
version = "1.24.3"
|
||||
version = "1.23.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coloredlogs" },
|
||||
{ name = "flatbuffers" },
|
||||
{ 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'" },
|
||||
@@ -742,30 +594,21 @@ dependencies = [
|
||||
{ name = "sympy" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/41/3253db975a90c3ce1d475e2a230773a21cd7998537f0657947df6fb79861/onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c", size = 17332766, upload-time = "2026-03-05T17:18:59.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/c5/3af6b325f1492d691b23844d88ed26844c1164620860c5efe95c0e22782d/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b2ebc54c6d8281dccff78d4b06e47d4cf07535937584ab759448390a70f4978", size = 15130330, upload-time = "2026-03-05T16:34:53.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/4b/f96b46c1866a293ed23ca2cf5e5a63d413ad3a951da60dd877e3c56cbbca/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb56575d7794bf0781156955610c9e651c9504c64d42ec880784b6106244882d", size = 17213247, upload-time = "2026-03-05T17:17:59.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/13/27cf4d8df2578747584e8758aeb0b673b60274048510257f1f084b15e80e/onnxruntime-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:c958222ef9eff54018332beecd32d5d94a3ab079d8821937b333811bf4da0d39", size = 12595530, upload-time = "2026-03-05T17:18:49.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/8c/6d9f31e6bae72a8079be12ed8ba36c4126a571fad38ded0a1b96f60f6896/onnxruntime-1.24.3-cp311-cp311-win_arm64.whl", hash = "sha256:a8f761857ebaf58a85b9e42422d03207f1d39e6bb8fecfdbf613bac5b9710723", size = 12261715, upload-time = "2026-03-05T17:18:39.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/7f/dfdc4e52600fde4c02d59bfe98c4b057931c1114b701e175aee311a9bc11/onnxruntime-1.24.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0d244227dc5e00a9ae15a7ac1eba4c4460d7876dfecafe73fb00db9f1d914d91", size = 17342578, upload-time = "2026-03-05T17:19:02.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/dc/1f5489f7b21817d4ad352bf7a92a252bd5b438bcbaa7ad20ea50814edc79/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a9847b870b6cb462652b547bc98c49e0efb67553410a082fde1918a38707452", size = 15150105, upload-time = "2026-03-05T16:34:56.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/7c/fd253da53594ab8efbefdc85b3638620ab1a6aab6eb7028a513c853559ce/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b354afce3333f2859c7e8706d84b6c552beac39233bcd3141ce7ab77b4cabb5d", size = 17237101, upload-time = "2026-03-05T17:18:02.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/5f/eaabc5699eeed6a9188c5c055ac1948ae50138697a0428d562ac970d7db5/onnxruntime-1.24.3-cp312-cp312-win_amd64.whl", hash = "sha256:44ea708c34965439170d811267c51281d3897ecfc4aa0087fa25d4a4c3eb2e4a", size = 12597638, upload-time = "2026-03-05T17:18:52.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/5c/d8066c320b90610dbeb489a483b132c3b3879b2f93f949fb5d30cfa9b119/onnxruntime-1.24.3-cp312-cp312-win_arm64.whl", hash = "sha256:48d1092b44ca2ba6f9543892e7c422c15a568481403c10440945685faf27a8d8", size = 12270943, upload-time = "2026-03-05T17:18:42.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/8d/487ece554119e2991242d4de55de7019ac6e47ee8dfafa69fcf41d37f8ed/onnxruntime-1.24.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:34a0ea5ff191d8420d9c1332355644148b1bf1a0d10c411af890a63a9f662aa7", size = 17342706, upload-time = "2026-03-05T16:35:10.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/25/8b444f463c1ac6106b889f6235c84f01eec001eaf689c3eff8c69cf48fae/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fd2ec7bb0fabe42f55e8337cfc9b1969d0d14622711aac73d69b4bd5abb5ed7", size = 15149956, upload-time = "2026-03-05T16:34:59.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/fc/c9182a3e1ab46940dd4f30e61071f59eee8804c1f641f37ce6e173633fb6/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df8e70e732fe26346faaeec9147fa38bef35d232d2495d27e93dd221a2d473a9", size = 17237370, upload-time = "2026-03-05T17:18:05.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/7e/3b549e1f4538514118bff98a1bcd6481dd9a17067f8c9af77151621c9a5c/onnxruntime-1.24.3-cp313-cp313-win_amd64.whl", hash = "sha256:2d3706719be6ad41d38a2250998b1d87758a20f6ea4546962e21dc79f1f1fd2b", size = 12597939, upload-time = "2026-03-05T17:18:54.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/41/9696a5c4631a0caa75cc8bc4efd30938fd483694aa614898d087c3ee6d29/onnxruntime-1.24.3-cp313-cp313-win_arm64.whl", hash = "sha256:b082f3ba9519f0a1a1e754556bc7e635c7526ef81b98b3f78da4455d25f0437b", size = 12270705, upload-time = "2026-03-05T17:18:44.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/65/a26c5e59e3b210852ee04248cf8843c81fe7d40d94cf95343b66efe7eec9/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72f956634bc2e4bd2e8b006bef111849bd42c42dea37bd0a4c728404fdaf4d34", size = 15161796, upload-time = "2026-03-05T16:35:02.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/25/2035b4aa2ccb5be6acf139397731ec507c5f09e199ab39d3262b22ffa1ac/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d1f25eed4ab9959db70a626ed50ee24cf497e60774f59f1207ac8556399c4d", size = 17240936, upload-time = "2026-03-05T17:18:09.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/a4/b3240ea84b92a3efb83d49cc16c04a17ade1ab47a6a95c4866d15bf0ac35/onnxruntime-1.24.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a6b4bce87d96f78f0a9bf5cefab3303ae95d558c5bfea53d0bf7f9ea207880a8", size = 17344149, upload-time = "2026-03-05T16:35:13.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/4a/4b56757e51a56265e8c56764d9c36d7b435045e05e3b8a38bedfc5aedba3/onnxruntime-1.24.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d48f36c87b25ab3b2b4c88826c96cf1399a5631e3c2c03cc27d6a1e5d6b18eb4", size = 15151571, upload-time = "2026-03-05T16:35:05.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/14/c6fb84980cec8f682a523fcac7c2bdd6b311e7f342c61ce48d3a9cb87fc6/onnxruntime-1.24.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e104d33a409bf6e3f30f0e8198ec2aaf8d445b8395490a80f6e6ad56da98e400", size = 17238951, upload-time = "2026-03-05T17:18:12.394Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/14/447e1400165aca8caf35dabd46540eb943c92f3065927bb4d9bcbc91e221/onnxruntime-1.24.3-cp314-cp314-win_amd64.whl", hash = "sha256:e785d73fbd17421c2513b0bb09eb25d88fa22c8c10c3f5d6060589efa5537c5b", size = 12903820, upload-time = "2026-03-05T17:18:57.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/ec/6b2fa5702e4bbba7339ca5787a9d056fc564a16079f8833cc6ba4798da1c/onnxruntime-1.24.3-cp314-cp314-win_arm64.whl", hash = "sha256:951e897a275f897a05ffbcaa615d98777882decaeb80c9216c68cdc62f849f53", size = 12594089, upload-time = "2026-03-05T17:18:47.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/dc/cd06cba3ddad92ceb17b914a8e8d49836c79e38936e26bde6e368b62c1fe/onnxruntime-1.24.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d4e70ce578aa214c74c7a7a9226bc8e229814db4a5b2d097333b81279ecde36", size = 15162789, upload-time = "2026-03-05T16:35:08.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/d6/413e98ab666c6fb9e8be7d1c6eb3bd403b0bea1b8d42db066dab98c7df07/onnxruntime-1.24.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02aaf6ddfa784523b6873b4176a79d508e599efe12ab0ea1a3a6e7314408b7aa", size = 17240738, upload-time = "2026-03-05T17:18:15.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/d6/311b1afea060015b56c742f3531168c1644650767f27ef40062569960587/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", size = 17195934, upload-time = "2025-10-27T23:06:14.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/db/81bf3d7cecfbfed9092b6b4052e857a769d62ed90561b410014e0aae18db/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36", size = 19153079, upload-time = "2025-10-27T23:05:57.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/4d/a382452b17cf70a2313153c520ea4c96ab670c996cb3a95cc5d5ac7bfdac/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2", size = 15219883, upload-time = "2025-10-22T03:46:21.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/56/179bf90679984c85b417664c26aae4f427cba7514bd2d65c43b181b7b08b/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7", size = 17370357, upload-time = "2025-10-22T03:46:57.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/6d/738e50c47c2fd285b1e6c8083f15dac1a5f6199213378a5f14092497296d/onnxruntime-1.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc", size = 13467651, upload-time = "2025-10-27T23:06:11.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/be/467b00f09061572f022ffd17e49e49e5a7a789056bad95b54dfd3bee73ff/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c", size = 17196113, upload-time = "2025-10-22T03:47:33.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/a8/3c23a8f75f93122d2b3410bfb74d06d0f8da4ac663185f91866b03f7da1b/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612", size = 19153857, upload-time = "2025-10-22T03:46:37.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/d8/506eed9af03d86f8db4880a4c47cd0dffee973ef7e4f4cff9f1d4bcf7d22/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6", size = 15220095, upload-time = "2025-10-22T03:46:24.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/80/113381ba832d5e777accedc6cb41d10f9eca82321ae31ebb6bcede530cea/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e", size = 17372080, upload-time = "2025-10-22T03:47:00.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/db/1b4a62e23183a0c3fe441782462c0ede9a2a65c6bbffb9582fab7c7a0d38/onnxruntime-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e", size = 13468349, upload-time = "2025-10-22T03:47:25.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/9e/f748cd64161213adeef83d0cb16cb8ace1e62fa501033acdd9f9341fff57/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321", size = 17195929, upload-time = "2025-10-22T03:47:36.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/9d/a81aafd899b900101988ead7fb14974c8a58695338ab6a0f3d6b0100f30b/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b", size = 19157705, upload-time = "2025-10-22T03:46:40.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/35/4e40f2fba272a6698d62be2cd21ddc3675edfc1a4b9ddefcc4648f115315/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c", size = 15226915, upload-time = "2025-10-22T03:46:27.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/88/9cc25d2bafe6bc0d4d3c1db3ade98196d5b355c0b273e6a5dc09c5d5d0d5/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77", size = 17382649, upload-time = "2025-10-22T03:47:02.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/b4/569d298f9fc4d286c11c45e85d9ffa9e877af12ace98af8cab52396e8f46/onnxruntime-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435", size = 13470528, upload-time = "2025-10-22T03:47:28.106Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -853,56 +696,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" },
|
||||
@@ -1001,48 +794,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
|
||||
@@ -1078,6 +829,15 @@ 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 = "pyreadline3"
|
||||
version = "3.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
@@ -1120,7 +880,7 @@ version = "0.52.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" }
|
||||
wheels = [
|
||||
@@ -1129,14 +889,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sympy"
|
||||
version = "1.14.0"
|
||||
version = "1.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mpmath" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040, upload-time = "2024-07-19T09:26:51.238Z" }
|
||||
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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177, upload-time = "2024-07-19T09:26:48.863Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1163,141 +923,129 @@ wheels = [
|
||||
{ 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"
|
||||
version = "2.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'linux')",
|
||||
"(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'linux')",
|
||||
"(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'linux')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "filelock" },
|
||||
{ name = "fsspec" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "filelock", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
|
||||
{ name = "fsspec", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
|
||||
{ name = "jinja2", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
|
||||
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'linux')" },
|
||||
{ name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'linux')" },
|
||||
{ name = "setuptools", marker = "(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'linux')" },
|
||||
{ name = "sympy", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
|
||||
{ name = "typing-extensions", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ef/834af4a885b31a0b32fff2d80e1e40f771e1566ea8ded55347502440786a/torch-2.5.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:71328e1bbe39d213b8721678f9dcac30dfc452a46d586f1d514a6aa0a99d4744", size = 906446312, upload-time = "2024-10-29T17:33:38.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/f0/46e74e0d145f43fa506cb336eaefb2d240547e4ce1f496e442711093ab25/torch-2.5.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:34bfa1a852e5714cbfa17f27c49d8ce35e1b7af5608c4bc6e81392c352dbc601", size = 91919522, upload-time = "2024-10-29T17:39:08.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/13/1eb674c8efbd04d71e4a157ceba991904f633e009a584dd65dccbafbb648/torch-2.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:32a037bd98a241df6c93e4c789b683335da76a2ac142c0973675b715102dc5fa", size = 203088048, upload-time = "2024-10-29T17:34:10.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/9d/e0860474ee0ff8f6ef2c50ec8f71a250f38d78a9b9df9fd241ad3397a65b/torch-2.5.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:23d062bf70776a3d04dbe74db950db2a5245e1ba4f27208a87f0d743b0d06e86", size = 63877046, upload-time = "2024-10-29T17:34:19.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/35/e8b2daf02ce933e4518e6f5682c72fd0ed66c15910ea1fb4168f442b71c4/torch-2.5.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:de5b7d6740c4b636ef4db92be922f0edc425b65ed78c5076c43c42d362a45457", size = 906474467, upload-time = "2024-10-29T17:38:49.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/04/bd91593a4ca178ece93ca55f27e2783aa524aaccbfda66831d59a054c31e/torch-2.5.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:340ce0432cad0d37f5a31be666896e16788f1adf8ad7be481196b503dad675b9", size = 91919450, upload-time = "2024-10-29T17:37:26.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/4a/e51420d46cfc90562e85af2fee912237c662ab31140ab179e49bd69401d6/torch-2.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:603c52d2fe06433c18b747d25f5c333f9c1d58615620578c326d66f258686f9a", size = 203098237, upload-time = "2024-10-29T17:36:11.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/db/5d9cbfbc7968d79c5c09a0bc0bc3735da079f2fd07cc10498a62b320a480/torch-2.5.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:31f8c39660962f9ae4eeec995e3049b5492eb7360dd4f07377658ef4d728fa4c", size = 63884466, upload-time = "2024-10-29T17:33:02.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/5c/36c114d120bfe10f9323ed35061bc5878cc74f3f594003854b0ea298942f/torch-2.5.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:ed231a4b3a5952177fafb661213d690a72caaad97d5824dd4fc17ab9e15cec03", size = 906389343, upload-time = "2024-10-29T17:37:06.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/69/d8ada8b6e0a4257556d5b4ddeb4345ea8eeaaef3c98b60d1cca197c7ad8e/torch-2.5.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:3f4b7f10a247e0dcd7ea97dc2d3bfbfc90302ed36d7f3952b0008d0df264e697", size = 91811673, upload-time = "2024-10-29T17:32:42.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/ba/607d013b55b9fd805db2a5c2662ec7551f1910b4eef39653eeaba182c5b2/torch-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:73e58e78f7d220917c5dbfad1a40e09df9929d3b95d25e57d9f8558f84c9a11c", size = 203046841, upload-time = "2024-10-29T17:35:48.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/6c/bf52ff061da33deb9f94f4121fde7ff3058812cb7d2036c97bc167793bd1/torch-2.5.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:8c712df61101964eb11910a846514011f0b6f5920c55dbf567bff8a34163d5b1", size = 63858109, upload-time = "2024-10-29T17:36:21.973Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "torch"
|
||||
version = "2.5.1+cu121"
|
||||
source = { registry = "https://download.pytorch.org/whl/cu121" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "filelock", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "fsspec", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "jinja2", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "setuptools", marker = "python_full_version >= '3.12'" },
|
||||
{ name = "sympy" },
|
||||
{ name = "setuptools", marker = "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "sympy", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-extensions", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/30/bfebdd8ec77db9a79775121789992d6b3b75ee5494971294d7b4b7c999bc/torch-2.10.0-2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2b980edd8d7c0a68c4e951ee1856334a43193f98730d97408fbd148c1a933313", size = 79411457, upload-time = "2026-02-10T21:44:59.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/1a/c61f36cfd446170ec27b3a4984f072fd06dab6b5d7ce27e11adb35d6c838/torch-2.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d", size = 145992962, upload-time = "2026-01-21T16:24:14.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/60/6662535354191e2d1555296045b63e4279e5a9dbad49acf55a5d38655a39/torch-2.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444", size = 915599237, upload-time = "2026-01-21T16:23:25.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/b8/66bbe96f0d79be2b5c697b2e0b187ed792a15c6c4b8904613454651db848/torch-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb", size = 113720931, upload-time = "2026-01-21T16:24:23.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/bb/d820f90e69cda6c8169b32a0c6a3ab7b17bf7990b8f2c680077c24a3c14c/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:35e407430795c8d3edb07a1d711c41cc1f9eaddc8b2f1cc0a165a6767a8fb73d", size = 79411450, upload-time = "2026-01-21T16:25:30.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/3d/c87b33c5f260a2a8ad68da7147e105f05868c281c63d65ed85aa4da98c66/torch-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd", size = 113723116, upload-time = "2026-01-21T16:25:21.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" },
|
||||
{ url = "https://download.pytorch.org/whl/cu121/torch-2.5.1%2Bcu121-cp310-cp310-linux_x86_64.whl", hash = "sha256:92af92c569de5da937dd1afb45ecfdd598ec1254cf2e49e3d698cb24d71aae14" },
|
||||
{ url = "https://download.pytorch.org/whl/cu121/torch-2.5.1%2Bcu121-cp311-cp311-linux_x86_64.whl", hash = "sha256:c8ab8c92eab928a93c483f83ca8c63f13dafc10fc93ad90ed2dcb7c82ea50410" },
|
||||
{ url = "https://download.pytorch.org/whl/cu121/torch-2.5.1%2Bcu121-cp312-cp312-linux_x86_64.whl", hash = "sha256:222be02548c2e74a21a8fbc8e5b8d2eef9f9faee865d70385d2eb1b9aabcbc76" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "torchvision"
|
||||
version = "0.25.0"
|
||||
version = "0.20.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'linux')",
|
||||
"(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'linux')",
|
||||
"(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'linux')",
|
||||
]
|
||||
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'" },
|
||||
{ name = "pillow" },
|
||||
{ name = "torch" },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'linux')" },
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'linux')" },
|
||||
{ name = "pillow", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
|
||||
{ name = "torch", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/ae/cbf727421eb73f1cf907fbe5788326a08f111b3f6b6ddca15426b53fec9a/torchvision-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a95c47abb817d4e90ea1a8e57bd0d728e3e6b533b3495ae77d84d883c4d11f56", size = 1874919, upload-time = "2026-01-21T16:27:47.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/68/dc7a224f606d53ea09f9a85196a3921ec3a801b0b1d17e84c73392f0c029/torchvision-0.25.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:acc339aba4a858192998c2b91f635827e40d9c469d9cf1455bafdda6e4c28ea4", size = 2343220, upload-time = "2026-01-21T16:27:44.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/fa/8cce5ca7ffd4da95193232493703d20aa06303f37b119fd23a65df4f239a/torchvision-0.25.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0d9a3f925a081dd2ebb0b791249b687c2ef2c2717d027946654607494b9b64b6", size = 8068106, upload-time = "2026-01-21T16:27:37.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/b9/a53bcf8f78f2cd89215e9ded70041765d50ef13bf301f9884ec6041a9421/torchvision-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:b57430fbe9e9b697418a395041bb615124d9c007710a2712fda6e35fb310f264", size = 3697295, upload-time = "2026-01-21T16:27:36.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/be/c704bceaf11c4f6b19d64337a34a877fcdfe3bd68160a8c9ae9bea4a35a3/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20", size = 1874923, upload-time = "2026-01-21T16:27:46.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/e9/f143cd71232430de1f547ceab840f68c55e127d72558b1061a71d0b193cd/torchvision-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3", size = 2344808, upload-time = "2026-01-21T16:27:43.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ae/ad5d6165797de234c9658752acb4fce65b78a6a18d82efdf8367c940d8da/torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee", size = 8070752, upload-time = "2026-01-21T16:27:33.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/19/55b28aecdc7f38df57b8eb55eb0b14a62b470ed8efeb22cdc74224df1d6a/torchvision-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:ea580ffd6094cc01914ad32f8c8118174f18974629af905cea08cb6d5d48c7b7", size = 4038722, upload-time = "2026-01-21T16:27:41.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/16/8f650c2e288977cf0f8f85184b90ee56ed170a4919347fc74ee99286ed6f/torchvision-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3", size = 4303059, upload-time = "2026-01-21T16:27:11.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a5/9a9b1de0720f884ea50dbf9acb22cbe5312e51d7b8c4ac6ba9b51efd9bba/torchvision-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f", size = 4321911, upload-time = "2026-01-21T16:27:35.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/cc/0ea68b5802e5e3c31f44b307e74947bad5a38cc655231d845534ed50ddb8/torchvision-0.25.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c", size = 4344260, upload-time = "2026-01-21T16:27:17.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/1f/fa839532660e2602b7e704d65010787c5bb296258b44fa8b9c1cd6175e7d/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1", size = 1896193, upload-time = "2026-01-21T16:27:24.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/ed/d51889da7ceaf5ff7a0574fb28f9b6b223df19667265395891f81b364ab3/torchvision-0.25.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce", size = 2309331, upload-time = "2026-01-21T16:27:19.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/a5/f93fcffaddd8f12f9e812256830ec9c9ca65abbf1bc369379f9c364d1ff4/torchvision-0.25.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03", size = 8088713, upload-time = "2026-01-21T16:27:15.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/eb/d0096eed5690d962853213f2ee00d91478dfcb586b62dbbb449fb8abc3a6/torchvision-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917", size = 4325058, upload-time = "2026-01-21T16:27:26.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/36/96374a4c7ab50dea9787ce987815614ccfe988a42e10ac1a2e3e5b60319a/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2", size = 1896207, upload-time = "2026-01-21T16:27:23.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/e2/7abb10a867db79b226b41da419b63b69c0bd5b82438c4a4ed50e084c552f/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563", size = 2310741, upload-time = "2026-01-21T16:27:18.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/e6/0927784e6ffc340b6676befde1c60260bd51641c9c574b9298d791a9cda4/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443", size = 8089772, upload-time = "2026-01-21T16:27:14.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/37/e7ca4ec820d434c0f23f824eb29f0676a0c3e7a118f1514f5b949c3356da/torchvision-0.25.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977", size = 4425879, upload-time = "2026-01-21T16:27:12.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/59/aea68d755da1451e1a0d894528a7edc9b58eb30d33e274bf21bef28dad1a/torchvision-0.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4878fefb96ef293d06c27210918adc83c399d9faaf34cda5a63e129f772328f1", size = 1787552, upload-time = "2024-10-29T17:40:34.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/f6/7ff89a9f8703f623f5664afd66c8600e3f09fe188e1e0b7e6f9a8617f865/torchvision-0.20.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:8ffbdf8bf5b30eade22d459f5a313329eeadb20dc75efa142987b53c007098c3", size = 7238975, upload-time = "2024-10-29T17:41:03.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ce/4c31e9b96cc4f9fec746b258d2aa35f8d1247f4f58d63f9c505ea5eb254d/torchvision-0.20.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:75f8a4d51a593c4bab6c9bf7d75bdd88691b00a53b07656678bc55a3a753dd73", size = 14265343, upload-time = "2024-10-29T17:40:57.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/11/b5ce67715bbbec8798fb48c4a20ac28828aec1710ac01091a3eddcb8e075/torchvision-0.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:22c2fa44e20eb404b85e42b22b453863a14b0927d25e550fd4f84eea97fa5b39", size = 1562413, upload-time = "2024-10-29T17:40:39.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/57/4d7ad90be612f5ac6c4bdafcb0ff13e818e14a340a88c8ca00d9ed8c2dad/torchvision-0.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:344b339e15e6bbb59ee0700772616d0afefd209920c762b1604368d8c3458322", size = 1787548, upload-time = "2024-10-29T17:40:55.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/e9/e190ecec448d5a2abad8348cf085fcb39962a491e3f40dcb023721e04feb/torchvision-0.20.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:86f6523dee420000fe14c3527f6c8e0175139fda7d995b187f54a0b0ebec7eb6", size = 7241222, upload-time = "2024-10-29T17:40:38.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/a3/cbb8177e5e379f0c040b00c6f80f14d323a97e30495d7115d169b101b2f7/torchvision-0.20.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:a40d766345927639da322c693934e5f91b1ba2218846c7104b868dea2314ce8e", size = 14267510, upload-time = "2024-10-29T17:40:53.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/55/ce836703ff77bb21582c3098d5311f8ddde7eadc7eab04be9561961f4725/torchvision-0.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:5b501d5c04b034d2ecda96a31ed050e383cf8201352e4c9276ca249cbecfded0", size = 1562402, upload-time = "2024-10-29T17:40:49.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/eb/4ba19616378f2bc085999432fded2b7dfdbdccc6dd0fc293203452508100/torchvision-0.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1a31256ff945d64f006bb306813a7c95a531fe16bfb2535c837dd4c104533d7a", size = 1787553, upload-time = "2024-10-29T17:40:50.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/75/00a852275ade58d3dc474530f7a7b6bc999a817148f0eb59d4fde12eb955/torchvision-0.20.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:17cd78adddf81dac57d7dccc9277a4d686425b1c55715f308769770cb26cad5c", size = 7240323, upload-time = "2024-10-29T17:40:44.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/f0/ca1445406eb12cbeb7a41fc833a1941ede78e7c55621198b83ecd7bcfd0f/torchvision-0.20.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:9f853ba4497ac4691815ad41b523ee23cf5ba4f87b1ce869d704052e233ca8b7", size = 14266936, upload-time = "2024-10-29T17:40:31.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/18/00993d420b1d6e88582e51d4bc82c824c99a2e9c045d50eaf9b34fff729a/torchvision-0.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:4a330422c36dbfc946d3a6c1caec3489db07ecdf3675d83369adb2e5a0ca17c4", size = 1562392, upload-time = "2024-10-29T17:40:47.6Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "torchvision"
|
||||
version = "0.20.1+cu121"
|
||||
source = { registry = "https://download.pytorch.org/whl/cu121" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
"python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "pillow", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "torch", version = "2.5.1+cu121", source = { registry = "https://download.pytorch.org/whl/cu121" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download.pytorch.org/whl/cu121/torchvision-0.20.1%2Bcu121-cp310-cp310-linux_x86_64.whl", hash = "sha256:304937b82c933d5155bd04d771f4b187273f67a76050bb4276b521f7e9b4c4e7" },
|
||||
{ url = "https://download.pytorch.org/whl/cu121/torchvision-0.20.1%2Bcu121-cp311-cp311-linux_x86_64.whl", hash = "sha256:237609a3551c2b68f32bcd3f3875dca93836010d83922ef2f3bd3a7540caee58" },
|
||||
{ url = "https://download.pytorch.org/whl/cu121/torchvision-0.20.1%2Bcu121-cp312-cp312-linux_x86_64.whl", hash = "sha256:48cf3a716f70370ed5dcb656e7497415ef37860b07e67ea4b1ef8598efe28445" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1314,16 +1062,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "triton"
|
||||
version = "3.6.0"
|
||||
version = "3.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/29/69aa56dc0b2eb2602b553881e34243475ea2afd9699be042316842788ff5/triton-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b0dd10a925263abbe9fa37dcde67a5e9b2383fc269fdf59f5657cac38c5d1d8", size = 209460013, upload-time = "2024-10-14T16:05:32.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/17/d9a5cf4fcf46291856d1e90762e36cbabd2a56c7265da0d1d9508c8e3943/triton-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f34f6e7885d1bf0eaaf7ba875a5f0ce6f3c13ba98f9503651c1e6dc6757ed5c", size = 209506424, upload-time = "2024-10-14T16:05:42.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/eb/65f5ba83c2a123f6498a3097746607e5b2f16add29e36765305e4ac7fdd8/triton-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8182f42fd8080a7d39d666814fa36c5e30cc00ea7eeeb1a2983dbb4c99a0fdc", size = 209551444, upload-time = "2024-10-14T16:05:53.433Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user