diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3891905..2d7cf53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -104,12 +104,12 @@ This package is small, but a few things in it are load-bearing in ways that are between the two copies always means the contract changed. It is excluded from `ruff format` for the same reason. If it fails, fix the engine. -**The labels in `src/guard_local/models.py` are a public API.** The client derives each result's task id from +**The labels in `src/guard_local/targets.py` are a public API.** The client derives each result's task id from `uuid5(namespace, label)`, so renaming a label silently changes the id that callers key on. They also mirror the tasks the cloud API seeds, which is what lets someone test locally and then route to the cloud unchanged. **The preprocessing must match the browser extension.** Both run the same ONNX file, and the scores only agree because -the transform agrees down to the rounding. Before touching `src/guard_local/media_utils.py`, read +the transform agrees down to the rounding. Before touching `src/guard_local/sources/model/transform.py`, read `src/lib/localModel/runner.ts` in `guard-browser-extension`. If a change is intentional, re-record the expected values in `test_golden_scores_for_a_deterministic_image` and say in the PR why the scores moved. diff --git a/README.md b/README.md index cef5d0f..ca3710c 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ for item in results: print(f"{item['label']}: {item['score']:.2f}") # AI-Generated: 0.90 -# Violence: 0.02 +# Violent: 0.02 # Explicit: 0.01 ``` diff --git a/pyproject.toml b/pyproject.toml index a4d8ef4..78b8627 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "guard-local-detector" -version = "0.0.1" +version = "0.0.2" description = "A local detection engine for the Guard Python client, integrating visual safety filters into your applications" readme = "README.md" requires-python = ">=3.10" @@ -45,7 +45,7 @@ dependencies = [ "pillow>=10.4.0", "pillow-heif>=1.0.0", "av>=14.0.0", - "c2pa-python>=0.37.0", + "c2pa-python>=0.38.0", ] [project.urls] diff --git a/src/guard_local/__init__.py b/src/guard_local/__init__.py index f690b0d..2da8f83 100644 --- a/src/guard_local/__init__.py +++ b/src/guard_local/__init__.py @@ -35,7 +35,7 @@ ModelLoadError, UnsupportedMediaError, ) -from .tasks import TASKS, Task +from .targets import TARGETS, Target try: __version__ = version("guard-local-detector") @@ -44,7 +44,7 @@ __all__ = [ "DETECTION_THRESHOLDS", - "TASKS", + "TARGETS", "CategoryResult", "GuardLocalError", "LocalDetectorEngine", @@ -52,7 +52,7 @@ "ModelLoadError", "Signal", "SignalMatch", - "Task", + "Target", "UnsupportedMediaError", "__version__", "passes_threshold", diff --git a/src/guard_local/detection/types.py b/src/guard_local/detection/types.py index 59160ea..95f4ae4 100644 --- a/src/guard_local/detection/types.py +++ b/src/guard_local/detection/types.py @@ -38,7 +38,7 @@ class Signal: Attributes: id: A stable identifier that is unique across every layer. Callers may branch on this, making it as much of a public API as the - task labels. + target labels. category: The category this signal evaluates. label: A short plain text name. description: An explanation of what the signal means and why it diff --git a/src/guard_local/engine.py b/src/guard_local/engine.py index 5f4122d..a0b6e59 100644 --- a/src/guard_local/engine.py +++ b/src/guard_local/engine.py @@ -30,7 +30,7 @@ from .exceptions import UnsupportedMediaError from .sources import C2paSource, MetadataSource, ModelSource, Source from .sources.model import AGGREGATORS -from .tasks import SUPPORTED_MEDIA_TYPES, TASKS, Task +from .targets import SUPPORTED_MEDIA_TYPES, TARGETS, Target __all__ = ["LocalDetectorEngine"] @@ -128,7 +128,7 @@ def __init__( def analyze(self, data: bytes, media_type: str) -> List[Dict[str, Any]]: """ - Score media against every detection task. + Score media against every detection target. Args: data: The raw media bytes to analyze. Still images are scored as a @@ -137,9 +137,9 @@ def analyze(self, data: bytes, media_type: str) -> List[Dict[str, Any]]: media_type: The MIME type of the media. Returns: - A list of results with one entry per task, always returned in the + A list of results with one entry per target, always returned in the same order. Each entry contains a `label`, a `score` representing a - probability from 0.0 to 1.0, a `description`, whether the task was + probability from 0.0 to 1.0, a `description`, whether the target was `detected`, and the `matches` that argued for it. Raises: @@ -159,17 +159,17 @@ def analyze(self, data: bytes, media_type: str) -> List[Dict[str, Any]]: source.analyze(data, media_type) for source in self.sources ) - return [self._report(task, found.get(task.category)) for task in TASKS] + return [self._report(target, found.get(target.category)) for target in TARGETS] @staticmethod - def _report(task: Task, evidence: Optional[CategoryResult]) -> Dict[str, Any]: + def _report(target: Target, evidence: Optional[CategoryResult]) -> Dict[str, Any]: """ - Phrase the merged evidence of one category as a task result. + Phrase the merged evidence of one category as a target result. Args: - task: The task being reported. + target: The target being reported. evidence: What the sources found, or `None` when no source spoke to this - task's category. + target's category. Returns: One result entry with the `score` constrained to the unit interval. @@ -181,9 +181,9 @@ def _report(task: Task, evidence: Optional[CategoryResult]) -> Dict[str, Any]: matches = evidence.matches if evidence else [] return { - "label": task.label, + "label": target.label, "score": min(1.0, max(0.0, confidence / 100.0)), - "description": task.description, - "detected": passes_threshold(task.category, confidence), + "description": target.description, + "detected": passes_threshold(target.category, confidence), "matches": [match.to_dict() for match in matches], } diff --git a/src/guard_local/sources/metadata/extract.py b/src/guard_local/sources/metadata/extract.py index aa94502..01b3e42 100644 --- a/src/guard_local/sources/metadata/extract.py +++ b/src/guard_local/sources/metadata/extract.py @@ -21,7 +21,7 @@ import pillow_heif from PIL import ExifTags, Image, ImageFile, IptcImagePlugin -from guard_local.tasks import VIDEO_MEDIA_TYPES +from guard_local.targets import VIDEO_MEDIA_TYPES __all__ = ["RawImageMetadata", "extract_image_metadata"] diff --git a/src/guard_local/sources/model/decode.py b/src/guard_local/sources/model/decode.py index 25687db..4397b93 100644 --- a/src/guard_local/sources/model/decode.py +++ b/src/guard_local/sources/model/decode.py @@ -24,7 +24,7 @@ MediaDecodeError, UnsupportedMediaError, ) -from guard_local.tasks import ( +from guard_local.targets import ( IMAGE_MEDIA_TYPES, SUPPORTED_MEDIA_TYPES, VIDEO_MEDIA_TYPES, diff --git a/src/guard_local/sources/model/source.py b/src/guard_local/sources/model/source.py index 5d203be..a5305e9 100644 --- a/src/guard_local/sources/model/source.py +++ b/src/guard_local/sources/model/source.py @@ -1,7 +1,7 @@ """ Scores pixels with the bundled multi-task vision model. -The model is a FastViT-T8 with one head per task. Because each head is trained with +The model is a FastViT-T8 with one head per target. Because each head is trained with `BCEWithLogits`, every output is an unbounded logit that requires a sigmoid activation to represent a probability. This is the only source that has an opinion about all three categories and the only one that must decode the media first. It is also the only source @@ -18,7 +18,7 @@ import numpy as np from guard_local.detection import CategoryResult, SignalMatch, passes_threshold -from guard_local.tasks import TASKS, VIDEO_MEDIA_TYPES +from guard_local.targets import TARGETS, VIDEO_MEDIA_TYPES from .decode import load_frames from .session import ModelSession @@ -78,7 +78,7 @@ def __init__( def analyze(self, data: bytes, media_type: str) -> Dict[str, CategoryResult]: """ - Decode the media and score every frame against every task. + Decode the media and score every frame against every target. Args: data: The raw media bytes. @@ -86,7 +86,7 @@ def analyze(self, data: bytes, media_type: str) -> Dict[str, CategoryResult]: Returns: One result per category. This always returns all three categories because - the model has an opinion about every task regardless of how confident it is. + the model has an opinion on every target regardless of how confident it is. Raises: ModelLoadError: If the model could not be found or loaded. @@ -98,13 +98,13 @@ def analyze(self, data: bytes, media_type: str) -> Dict[str, CategoryResult]: frames = load_frames(data, media_type, max_frames=max_frames) session, input_name = self.session.ensure() - outputs = [task.output for task in TASKS] + outputs = [target.output for target in TARGETS] per_frame = [ session.run(outputs, {input_name: to_tensor(frame)}) for frame in frames ] results: Dict[str, CategoryResult] = {} - for index, task in enumerate(TASKS): + for index, target in enumerate(TARGETS): score = self._aggregate( [ _sigmoid(float(np.asarray(result[index]).reshape(-1)[0])) @@ -112,23 +112,23 @@ def analyze(self, data: bytes, media_type: str) -> Dict[str, CategoryResult]: ] ) confidence = score * 100.0 - results[task.category] = CategoryResult( - detected=passes_threshold(task.category, confidence), + results[target.category] = CategoryResult( + detected=passes_threshold(target.category, confidence), # Kept as the probability the model produced rather than the whole # percent the match displays. The browser extension rounds before # comparing sources; comparing at full precision picks the same winner # and spares the caller a rounding it never asked for. confidence=confidence, - matches=[self._match(task.category, score)], + matches=[self._match(target.category, score)], ) return results def _aggregate(self, scores: Sequence[float]) -> float: """ - Combine the per-frame scores of a single task into the final model score. + Combine the per-frame scores of a single target into the final model score. Args: - scores: The probabilities calculated for every frame on a single task. + scores: The probabilities calculated for every frame on a single target. Returns: A probability between 0.0 and 1.0. diff --git a/src/guard_local/tasks.py b/src/guard_local/targets.py similarity index 74% rename from src/guard_local/tasks.py rename to src/guard_local/targets.py index 337be48..9bd90db 100644 --- a/src/guard_local/tasks.py +++ b/src/guard_local/targets.py @@ -1,11 +1,11 @@ """ -The detection tasks and media types mirroring the vocabulary of the cloud API. +The detection targets and media types mirroring the vocabulary of the cloud API. -Labels in this module act as a public API. The Guard client assigns local results a task -ID of `uuid5(namespace, label)`. Because of this, renaming a label will silently change -the ID that callers rely on. These labels match the exact tasks seeded by the backend. -This consistency allows developers to test against the local engine and then transition -to the cloud without writing conditional logic for which engine ran. +Labels in this module act as a public API. The Guard client assigns local results a +target ID of `uuid5(namespace, label)`. Because of this, renaming a label will silently +change the ID that callers rely on. These labels match the exact tasks seeded by the +backend. This consistency allows developers to test against the local engine and then +transition to the cloud without writing conditional logic for which engine ran. """ from __future__ import annotations @@ -18,22 +18,22 @@ __all__ = [ "IMAGE_MEDIA_TYPES", "SUPPORTED_MEDIA_TYPES", - "TASKS", + "TARGETS", "VIDEO_MEDIA_TYPES", - "Task", + "Target", ] @dataclass(frozen=True) -class Task: +class Target: """ One detection head of the model sharing the same name as the cloud API. Attributes: - label: The plain text detection name and the seed used for the task ID. + label: The plain text detection name and the seed used for the target ID. description: The detailed explanation returned by the cloud API. output: The name of the ONNX graph output carrying the logit for this head. - category: The detection category this task reports, matched to the naming + category: The detection category this target reports, matched to the naming convention of the metadata and C2PA layers. This is the only place where the vocabulary of the cloud API and the signal vocabulary are tied together. @@ -46,20 +46,20 @@ class Task: #: The three heads in the exact order results are always reported. -TASKS: Tuple[Task, ...] = ( - Task( +TARGETS: Tuple[Target, ...] = ( + Target( label="AI-Generated", description="Detect AI-generated or manipulated media", output="out_ai", category=AI_GENERATED, ), - Task( - label="Violence", + Target( + label="Violent", description="Detect violent media", output="out_violence", category=VIOLENT, ), - Task( + Target( label="Explicit", description="Detect sexually explicit media", output="out_nsfw", diff --git a/tests/test_contract.py b/tests/test_contract.py index f43bdf1..3e67c96 100644 --- a/tests/test_contract.py +++ b/tests/test_contract.py @@ -85,7 +85,7 @@ def test_labels_are_stable_across_calls() -> None: """ Ensure labels remain entirely stable across multiple calls. - Local task IDs are generated using a UUID5 hash of the label. Because + Local target IDs are generated using a UUID5 hash of the label. Because of this, silently changing a label would inadvertently change the ID. """ def labels(raw: Union[Dict[str, Any], List[Dict[str, Any]]]) -> List[str]: diff --git a/tests/test_engine.py b/tests/test_engine.py index 3c7006c..0d116b8 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -16,7 +16,7 @@ ModelLoadError, UnsupportedMediaError, ) -from guard_local.tasks import SUPPORTED_MEDIA_TYPES, TASKS +from guard_local.targets import SUPPORTED_MEDIA_TYPES, TARGETS from .conftest import jpeg_bytes, png_bytes, png_with_text @@ -55,14 +55,11 @@ def test_rejects_having_every_layer_turned_off(self) -> None: class TestAnalyze: - def test_reports_every_task_in_a_stable_order(self, engine: Any) -> None: + def test_reports_every_target_in_a_stable_order(self, engine: Any) -> None: results = engine.analyze(png_bytes(), "image/png") - assert [entry["label"] for entry in results] == [task.label for task in TASKS] assert [entry["label"] for entry in results] == [ - "AI-Generated", - "Violence", - "Explicit", + target.label for target in TARGETS ] def test_carries_the_cloud_descriptions(self, engine: Any) -> None: @@ -77,16 +74,16 @@ def test_scores_are_floats_in_the_unit_interval(self, engine: Any) -> None: assert isinstance(entry["score"], float) assert 0.0 <= entry["score"] <= 1.0 - def test_emits_no_task_id_so_the_client_derives_one(self, engine: Any) -> None: + def test_emits_no_target_id_so_the_client_derives_one(self, engine: Any) -> None: """ - Ensure no task ID is emitted so the client derives it itself. + Ensure no target ID is emitted so the client derives it itself. Using a UUID5 hash of the label keeps IDs reproducible. Emitting an ID directly from the engine would only introduce noise. """ results = engine.analyze(png_bytes(), "image/png") - assert all("task_id" not in entry for entry in results) + assert all("target_id" not in entry for entry in results) def test_the_same_bytes_score_the_same(self, engine: Any) -> None: data = jpeg_bytes() @@ -122,7 +119,7 @@ class TestVideo: def test_a_clip_is_scored_across_frames(self, engine: Any, mp4: Any) -> None: results = engine.analyze(mp4, "video/mp4") - assert len(results) == len(TASKS) + assert len(results) == len(TARGETS) assert all(0.0 <= entry["score"] <= 1.0 for entry in results) def test_the_video_knobs_reach_the_model_source(self, mp4: Any) -> None: @@ -230,7 +227,7 @@ def test_detected_follows_the_category_threshold(self, engine: Any) -> None: assert results[0]["score"] == pytest.approx(0.25) assert results[0]["detected"] is False - def test_a_task_with_nothing_to_report_is_a_clean_zero(self) -> None: + def test_a_target_with_nothing_to_report_is_a_clean_zero(self) -> None: quiet = LocalDetectorEngine(use_model=False) violence = quiet.analyze(png_bytes(), "image/png")[1] @@ -294,13 +291,13 @@ def test_golden_scores_for_a_deterministic_image(engine: Any) -> None: } assert results["AI-Generated"] == pytest.approx(GOLDEN["AI-Generated"], abs=1e-4) - assert results["Violence"] == pytest.approx(GOLDEN["Violence"], abs=1e-4) + assert results["Violent"] == pytest.approx(GOLDEN["Violent"], abs=1e-4) assert results["Explicit"] == pytest.approx(GOLDEN["Explicit"], abs=1e-4) #: Recorded from the bundled model; see the docstring above for why they are pinned. GOLDEN = { "AI-Generated": 0.257501, - "Violence": 0.000646, + "Violent": 0.000646, "Explicit": 0.033055, } diff --git a/uv.lock b/uv.lock index e9492ce..8954030 100644 --- a/uv.lock +++ b/uv.lock @@ -122,25 +122,22 @@ wheels = [ [[package]] name = "c2pa-python" -version = "0.37.4" +version = "0.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, - { name = "pytest" }, { name = "requests" }, - { name = "setuptools" }, { name = "toml" }, - { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/6c/0e28502553bc32dc98f38a5415e20fa444e15e19ee7d17c8992cc8b23a28/c2pa_python-0.37.4.tar.gz", hash = "sha256:6aebfe6f669077d0662dcd9ad3612c40baf888ae22d2f4866d383a9f7dd46aee", size = 112919, upload-time = "2026-08-04T21:30:37.317Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/b1/2ff409fdfe7304fc2e4074ebbe8079274e21bdbbf60b34bd7f9a3e60670f/c2pa_python-0.38.0.tar.gz", hash = "sha256:3a40dab7424e03b397f5c4c81298d7f534a28e7d75a17d5d39fdc35c5dfe5963", size = 137647, upload-time = "2026-09-29T21:05:01.918Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/b6/3b8041a3d163168489d1458e63c56cb7a782d36298520f8eac83b7da928c/c2pa_python-0.37.4-py3-none-macosx_10_9_universal2.whl", hash = "sha256:d2814c7d9c2759a7dec6404779aead36104fe807886ecf6c6cbd2af26c2ab3f0", size = 16183400, upload-time = "2026-08-04T21:30:13.39Z" }, - { url = "https://files.pythonhosted.org/packages/d2/22/199a897c0b526fec6c109c947db6c2010da70b782841e9a3f6c72af026de/c2pa_python-0.37.4-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:839123d8854802848695ce24c7dd56661be56fbb815818de19f0d6a6d8d2bc16", size = 14148222, upload-time = "2026-08-04T21:30:16.092Z" }, - { url = "https://files.pythonhosted.org/packages/a7/79/7f9c670d644eb1681cf304fc22d5b31ea26cf9b9bece37a2ca4b1c5cd624/c2pa_python-0.37.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:243260fdd577549185175daaf9fdb4f30c8e940aaaf555b34c3579c6b4549c6c", size = 13782417, upload-time = "2026-08-04T21:30:18.885Z" }, - { url = "https://files.pythonhosted.org/packages/73/f2/4ba5294e9f32d97bb79ae8ea6ca0c9401c36e4e783fef3dcf1ebe6099830/c2pa_python-0.37.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:baa39c2befedcb1c45c0cb439a3b2b4f0c428ab3126e56c02b59a2e3700212f9", size = 13987587, upload-time = "2026-08-04T21:30:21.456Z" }, - { url = "https://files.pythonhosted.org/packages/79/72/be5e4db496a4c52134970ffcd14bd557c7c25c6b9fa6f55166c08776a80f/c2pa_python-0.37.4-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:8e2b1b214ea4e885178e1bd6c330d2c4c0c0d3aaa9cdb3bc28f7e7a7144ee5c0", size = 14636994, upload-time = "2026-08-04T21:30:24.021Z" }, - { url = "https://files.pythonhosted.org/packages/56/ef/575cfa3f60be714056fa1e6118375b92dbc252c3a63e0d37893a521f62e5/c2pa_python-0.37.4-py3-none-win_amd64.whl", hash = "sha256:4133e8e99059eeb8be0023fcb01aea17a588fe23911458813d6dc451d6eff712", size = 86337929, upload-time = "2026-08-04T21:30:28.796Z" }, - { url = "https://files.pythonhosted.org/packages/19/b6/f933ea182270f8285c66181a420a8fbcfa8a83ba590f265dd5dc864e3c26/c2pa_python-0.37.4-py3-none-win_arm64.whl", hash = "sha256:e183af9a6d511352cc427bed22611ea1b9dd1dd13620ec5555fb6c12643e6779", size = 84009543, upload-time = "2026-08-04T21:30:34.501Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d9/90ad8810bb0638da95bd5ad814dd15cb843886edb9b6eeab3f1c6c139db1/c2pa_python-0.38.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:334ffbff033423c910f1e635447cd775c1d66874e02b6db55d6f62d7a5a856fc", size = 16937345, upload-time = "2026-09-29T21:04:41.275Z" }, + { url = "https://files.pythonhosted.org/packages/69/1c/2fab71e40c510ccaad4f2032f751f336d027b2235f10fe41f09cf44fffb7/c2pa_python-0.38.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d1b5522673c0df8b64f85fc9f55bab5f211920fb571669bd8461a8ffe7e3a287", size = 14806621, upload-time = "2026-09-29T21:04:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/09/3c/04b75a601202c8e90116472e325dc2ae2c1f371153b8b8244e8b1b619ca2/c2pa_python-0.38.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53364e6b02b2c26863419f444bb6390e8c4c4095eb0e3c3373c7144e7a7ff363", size = 14451808, upload-time = "2026-09-29T21:04:46.24Z" }, + { url = "https://files.pythonhosted.org/packages/eb/88/74711ff71b5c4448322e5af27160e5f4032bda8dec9ab4c6534733e119b9/c2pa_python-0.38.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:dcdc16c23178cb580c1017dca88ca2f6f516b3748ecf4c69acfff78ea385feb8", size = 14677363, upload-time = "2026-09-29T21:04:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/85a91e6262e0399e88596f79b73d2953f68db98aa1c8b18225121c7bcd70/c2pa_python-0.38.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:9739589e69e5891da80d9b2cc2549c8a9b224d9131cafed4582d5746a155fd2d", size = 15334006, upload-time = "2026-09-29T21:04:51.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/9b/075dac1e82584faa7b69dfa08d101d9c82872972f3f4a8a93a117c706d97/c2pa_python-0.38.0-py3-none-win_amd64.whl", hash = "sha256:5db598a420242229a802c07e84890adb5162aac79afb110e4563c56f33e35256", size = 87904872, upload-time = "2026-09-29T21:04:54.755Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ce/a4550a56b5f1b1f898c1081145606d6a64e26d93eb7c6bf51f11da899d4a/c2pa_python-0.38.0-py3-none-win_arm64.whl", hash = "sha256:abac93851169c016aadb7251177656c3919af5ce469c011ff4ea014bd02501b4", size = 85299399, upload-time = "2026-09-29T21:04:59.1Z" }, ] [[package]] @@ -484,7 +481,7 @@ wheels = [ [[package]] name = "guard-local-detector" -version = "0.0.1" +version = "0.0.2" source = { editable = "." } dependencies = [ { name = "av", version = "17.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -513,7 +510,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "av", specifier = ">=14.0.0" }, - { name = "c2pa-python", specifier = ">=0.37.0" }, + { name = "c2pa-python", specifier = ">=0.38.0" }, { name = "numpy", specifier = ">=1.24.4" }, { name = "onnxruntime", marker = "python_full_version < '3.11'", specifier = ">=1.20.1,<1.24" }, { name = "onnxruntime", marker = "python_full_version >= '3.11'", specifier = ">=1.20.1" }, @@ -1297,15 +1294,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" }, ] -[[package]] -name = "setuptools" -version = "83.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, -] - [[package]] name = "sympy" version = "1.14.0" @@ -1398,15 +1386,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] - -[[package]] -name = "wheel" -version = "0.47.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, -]