From 99d02f28c8d34610ab35559b72642b01f10fdb1e Mon Sep 17 00:00:00 2001 From: generatedunixname537391475639613 Date: Tue, 27 Jan 2026 04:54:06 -0800 Subject: [PATCH 01/31] fbcode/deeplearning/projects/sam3_release/sam3/train/data Reviewed By: JuanBesa Differential Revision: D91383480 fbshipit-source-id: 5b98627fb679c7c704c1a2faba9722e3a6f2ec20 --- sam3/train/data/sam3_image_dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sam3/train/data/sam3_image_dataset.py b/sam3/train/data/sam3_image_dataset.py index c5a1c83..c956d7b 100644 --- a/sam3/train/data/sam3_image_dataset.py +++ b/sam3/train/data/sam3_image_dataset.py @@ -328,7 +328,7 @@ def load_queries(self, pil_images, annotations, queries, img_metadata): f"Number of queries in stage {stage} is {num_queries}, expected {num_queries_per_stage}" ) - for query_id, query in enumerate(queries): + for query in queries: h, w = id2imsize[query["image_id"]] if ( "input_box" in query From 2ec3c0711a9719ac324388adc662e471f8fe2168 Mon Sep 17 00:00:00 2001 From: generatedunixname537391475639613 Date: Tue, 3 Feb 2026 13:14:44 -0800 Subject: [PATCH 02/31] fbcode/deeplearning/projects/sam3_release/sam3/train/data Reviewed By: JuanBesa Differential Revision: D91210167 fbshipit-source-id: a563232f4bc82f6f3b99e53df1c88cf0f39747bb --- sam3/train/data/collator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sam3/train/data/collator.py b/sam3/train/data/collator.py index 4a0f2e8..38d031d 100644 --- a/sam3/train/data/collator.py +++ b/sam3/train/data/collator.py @@ -194,7 +194,7 @@ def collate_fn_api( offset_img_id = 0 offset_query_id = [0 for _ in range(num_stages)] - for i, data in enumerate(batch): + for data in batch: img_batch.extend([img.data for img in data.images]) if data.raw_images is not None: @@ -209,7 +209,7 @@ def collate_fn_api( datapoint_query_id_2_stage_query_id.append(offset_query_id[stage_id]) offset_query_id[stage_id] += 1 - for j, q in enumerate(data.find_queries): + for q in data.find_queries: stage_id = q.query_processing_order stages[stage_id].img_ids.append(q.image_id + offset_img_id) if q.query_text not in text_batch: From ae2931624046444c57e02c3306b2060db8d9a8ac Mon Sep 17 00:00:00 2001 From: Ryan Avery Date: Wed, 4 Feb 2026 20:24:19 -0800 Subject: [PATCH 03/31] Fix export edge cases in encoder/decoder --- sam3/model/decoder.py | 20 +- sam3/model/encoder.py | 2 +- sam3/model/geometry_encoders.py | 12 +- sam3/model/position_encoding.py | 6 +- tests/__init__.py | 0 tests/export/__init__.py | 0 tests/export/conftest.py | 31 +++ ...amic_batch-export_dynamic_batch.stderr.txt | 0 ...08-1008_-export_dynamic_spatial.stderr.txt | 0 ...44-1344_-export_dynamic_spatial.stderr.txt | 0 ...ull_dynamic-export_full_dynamic.stderr.txt | 0 ...4-1344_-export_inference_shapes.stderr.txt | 0 ...8-1008_-export_inference_shapes.stderr.txt | 0 ...coder_export_loads-export_loads.stderr.txt | 0 ...port_matches_eager-export_match.stderr.txt | 0 ...der_export_static-export_static.stderr.txt | 0 ...amic_batch-export_dynamic_batch.stderr.txt | 0 ..._spatial-export_dynamic_spatial.stderr.txt | 0 ...l_16-16_-export_dynamic_spatial.stderr.txt | 0 ...l_36-36_-export_dynamic_spatial.stderr.txt | 0 ...l_72-72_-export_dynamic_spatial.stderr.txt | 0 ...6-16-4_-export_inference_shapes.stderr.txt | 0 ...es_1-4_-export_inference_shapes.stderr.txt | 0 ...6-36-8_-export_inference_shapes.stderr.txt | 0 ...es_2-8_-export_inference_shapes.stderr.txt | 0 ...coder_export_loads-export_loads.stderr.txt | 0 ...der_export_static-export_static.stderr.txt | 0 ...der_export_static-export_static.stderr.txt | 0 ...amic_batch-export_dynamic_batch.stderr.txt | 0 ..._len_16_-export_dynamic_seq_len.stderr.txt | 0 ..._len_32_-export_dynamic_seq_len.stderr.txt | 0 ...q_len_8_-export_dynamic_seq_len.stderr.txt | 0 ...s_1-32_-export_inference_shapes.stderr.txt | 0 ...es_1-8_-export_inference_shapes.stderr.txt | 0 ...s_2-16_-export_inference_shapes.stderr.txt | 0 ...s_2-32_-export_inference_shapes.stderr.txt | 0 ...s_3-32_-export_inference_shapes.stderr.txt | 0 ...coder_export_loads-export_loads.stderr.txt | 0 ...port_matches_eager-export_match.stderr.txt | 0 ...der_export_static-export_static.stderr.txt | 0 tests/export/test_decoder_export.py | 223 ++++++++++++++++++ tests/export/test_encoder_export.py | 179 ++++++++++++++ tests/export/test_image_encoder_export.py | 137 +++++++++++ tests/export/test_text_encoder_export.py | 127 ++++++++++ tests/export/utils.py | 43 ++++ 45 files changed, 760 insertions(+), 20 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/export/__init__.py create mode 100644 tests/export/conftest.py create mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_batch-export_dynamic_batch.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_spatial_1008-1008_-export_dynamic_spatial.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_spatial_1344-1344_-export_dynamic_spatial.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_full_dynamic-export_full_dynamic.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_inference_shapes_1-1344-1344_-export_inference_shapes.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_inference_shapes_2-1008-1008_-export_inference_shapes.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_loads-export_loads.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_matches_eager-export_match.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_static-export_static.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_batch-export_dynamic_batch.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial-export_dynamic_spatial.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_16-16_-export_dynamic_spatial.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_36-36_-export_dynamic_spatial.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_72-72_-export_dynamic_spatial.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_1-16-16-4_-export_inference_shapes.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_1-4_-export_inference_shapes.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_2-36-36-8_-export_inference_shapes.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_2-8_-export_inference_shapes.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_loads-export_loads.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_static-export_static.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_image_encoder_export.py_test_image_encoder_export_static-export_static.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_batch-export_dynamic_batch.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_16_-export_dynamic_seq_len.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_32_-export_dynamic_seq_len.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_8_-export_dynamic_seq_len.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_1-32_-export_inference_shapes.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_1-8_-export_inference_shapes.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_2-16_-export_inference_shapes.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_2-32_-export_inference_shapes.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_3-32_-export_inference_shapes.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_loads-export_loads.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_matches_eager-export_match.stderr.txt create mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_static-export_static.stderr.txt create mode 100644 tests/export/test_decoder_export.py create mode 100644 tests/export/test_encoder_export.py create mode 100644 tests/export/test_image_encoder_export.py create mode 100644 tests/export/test_text_encoder_export.py create mode 100644 tests/export/utils.py diff --git a/sam3/model/decoder.py b/sam3/model/decoder.py index 7a204be..4ecfe91 100644 --- a/sam3/model/decoder.py +++ b/sam3/model/decoder.py @@ -151,7 +151,7 @@ def forward( tgt = tgt + self.catext_dropout(tgt2) tgt = self.catext_norm(tgt) - if presence_token is not None: + if presence_token is not None and cross_attn_mask is not None: presence_token_mask = torch.zeros_like(cross_attn_mask[:, :1, :]) cross_attn_mask = torch.cat( [presence_token_mask, cross_attn_mask], dim=1 @@ -333,10 +333,7 @@ def _get_rpb_matrix(self, reference_boxes, feat_size): self.compilable_cord_cache = self._get_coords(H, W, reference_boxes.device) self.compilable_stored_size = (H, W) - if torch.compiler.is_dynamo_compiling() or self.compilable_stored_size == ( - H, - W, - ): + if torch.compiler.is_dynamo_compiling(): # good, hitting the cache, will be compilable coords_h, coords_w = self.compilable_cord_cache else: @@ -348,8 +345,6 @@ def _get_rpb_matrix(self, reference_boxes, feat_size): ) coords_h, coords_w = self.coord_cache[feat_size] - assert coords_h.shape == (H,) - assert coords_w.shape == (W,) deltas_y = coords_h.view(1, -1, 1) - boxes_xyxy.reshape(-1, 1, 4)[:, :, 1:4:2] deltas_y = deltas_y.view(bs, num_queries, -1, 2) @@ -388,20 +383,13 @@ def _get_rpb_matrix(self, reference_boxes, feat_size): act_ckpt_enable=self.training and self.use_act_checkpoint, ) # bs, num_queries, H, n_heads - if not torch.compiler.is_dynamo_compiling(): - assert deltas_x.shape[:3] == (bs, num_queries, W) - assert deltas_y.shape[:3] == (bs, num_queries, H) B = deltas_y.unsqueeze(3) + deltas_x.unsqueeze( 2 ) # bs, num_queries, H, W, n_heads - if not torch.compiler.is_dynamo_compiling(): - assert B.shape[:4] == (bs, num_queries, H, W) B = B.flatten(2, 3) # bs, num_queries, H*W, n_heads B = B.permute(0, 3, 1, 2) # bs, n_heads, num_queries, H*W B = B.contiguous() # memeff attn likes ordered strides - if not torch.compiler.is_dynamo_compiling(): - assert B.shape[2:] == (num_queries, H * W) return B def forward( @@ -510,7 +498,9 @@ def forward( # conditional query query_pos = self.ref_point_head(query_sine_embed) # nq, bs, d_model - if self.boxRPB != "none" and reference_boxes is not None: + compiling = torch.compiler.is_dynamo_compiling() or torch._dynamo.is_compiling() + memory_mask = None + if self.boxRPB != "none" and reference_boxes is not None and not compiling: assert spatial_shapes.shape[0] == 1, ( "only single scale support implemented" ) diff --git a/sam3/model/encoder.py b/sam3/model/encoder.py index 3fc9406..49b0219 100644 --- a/sam3/model/encoder.py +++ b/sam3/model/encoder.py @@ -538,7 +538,7 @@ def forward( else None ) else: - assert all(x.dim == 4 for x in src), ( + assert all(x.dim() == 4 for x in src), ( "expected list of (bs, c, h, w) tensors" ) diff --git a/sam3/model/geometry_encoders.py b/sam3/model/geometry_encoders.py index d60ee54..fda880e 100644 --- a/sam3/model/geometry_encoders.py +++ b/sam3/model/geometry_encoders.py @@ -645,7 +645,17 @@ def _encode_boxes(self, boxes, boxes_mask, boxes_labels, img_feats): # We need to denormalize, and convert to [x, y, x, y] boxes_xyxy = box_cxcywh_to_xyxy(boxes) scale = torch.tensor([W, H, W, H], dtype=boxes_xyxy.dtype) - scale = scale.pin_memory().to(device=boxes_xyxy.device, non_blocking=True) + if ( + torch.is_tensor(scale) + and scale.device.type == "cpu" + and boxes_xyxy.device.type != "cpu" + and not torch._dynamo.is_compiling() + ): + scale = scale.pin_memory() + scale = scale.to( + device=boxes_xyxy.device, + non_blocking=boxes_xyxy.device.type != "cpu" and not torch._dynamo.is_compiling(), + ) scale = scale.view(1, 1, 4) boxes_xyxy = boxes_xyxy * scale sampled = torchvision.ops.roi_align( diff --git a/sam3/model/position_encoding.py b/sam3/model/position_encoding.py index a6a1266..7419242 100644 --- a/sam3/model/position_encoding.py +++ b/sam3/model/position_encoding.py @@ -89,9 +89,9 @@ def encode_points(self, x, y, labels): @torch.no_grad() def forward(self, x): - cache_key = None cache_key = (x.shape[-2], x.shape[-1]) - if cache_key in self.cache: + use_cache = all(isinstance(dim, int) for dim in cache_key) + if use_cache and cache_key in self.cache: return self.cache[cache_key][None].repeat(x.shape[0], 1, 1, 1) y_embed = ( torch.arange(1, x.shape[-2] + 1, dtype=torch.float32, device=x.device) @@ -121,6 +121,6 @@ def forward(self, x): (pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4 ).flatten(3) pos = torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2) - if cache_key is not None: + if use_cache: self.cache[cache_key] = pos[0] return pos diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/__init__.py b/tests/export/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/conftest.py b/tests/export/conftest.py new file mode 100644 index 0000000..00ab475 --- /dev/null +++ b/tests/export/conftest.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import os +import sys +from pathlib import Path + +import pytest +import torch + +REPO_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(REPO_ROOT)) + +from sam3.model_builder import build_sam3_image_model + + +@pytest.fixture(scope="session") +def sam3_model() -> torch.nn.Module: + force_cpu = os.getenv("SAM3_EXPORT_FORCE_CPU", "0") == "1" + device = os.getenv("SAM3_EXPORT_DEVICE") + if device is None: + device = "cuda" if torch.cuda.is_available() and not force_cpu else "cpu" + try: + model = build_sam3_image_model( + device=device, eval_mode=True, enable_segmentation=True + ) + except torch.OutOfMemoryError: + if device == "cuda": + torch.cuda.empty_cache() + pytest.skip("CUDA OOM while loading SAM3 model; free GPU memory or set SAM3_EXPORT_FORCE_CPU=1") + model.eval() + return model diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_batch-export_dynamic_batch.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_batch-export_dynamic_batch.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_spatial_1008-1008_-export_dynamic_spatial.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_spatial_1008-1008_-export_dynamic_spatial.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_spatial_1344-1344_-export_dynamic_spatial.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_spatial_1344-1344_-export_dynamic_spatial.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_full_dynamic-export_full_dynamic.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_full_dynamic-export_full_dynamic.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_inference_shapes_1-1344-1344_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_inference_shapes_1-1344-1344_-export_inference_shapes.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_inference_shapes_2-1008-1008_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_inference_shapes_2-1008-1008_-export_inference_shapes.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_loads-export_loads.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_loads-export_loads.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_matches_eager-export_match.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_matches_eager-export_match.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_static-export_static.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_static-export_static.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_batch-export_dynamic_batch.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_batch-export_dynamic_batch.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial-export_dynamic_spatial.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial-export_dynamic_spatial.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_16-16_-export_dynamic_spatial.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_16-16_-export_dynamic_spatial.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_36-36_-export_dynamic_spatial.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_36-36_-export_dynamic_spatial.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_72-72_-export_dynamic_spatial.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_72-72_-export_dynamic_spatial.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_1-16-16-4_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_1-16-16-4_-export_inference_shapes.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_1-4_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_1-4_-export_inference_shapes.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_2-36-36-8_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_2-36-36-8_-export_inference_shapes.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_2-8_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_2-8_-export_inference_shapes.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_loads-export_loads.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_loads-export_loads.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_static-export_static.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_static-export_static.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_image_encoder_export.py_test_image_encoder_export_static-export_static.stderr.txt b/tests/export/export_logs/tests_export_test_image_encoder_export.py_test_image_encoder_export_static-export_static.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_batch-export_dynamic_batch.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_batch-export_dynamic_batch.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_16_-export_dynamic_seq_len.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_16_-export_dynamic_seq_len.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_32_-export_dynamic_seq_len.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_32_-export_dynamic_seq_len.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_8_-export_dynamic_seq_len.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_8_-export_dynamic_seq_len.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_1-32_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_1-32_-export_inference_shapes.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_1-8_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_1-8_-export_inference_shapes.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_2-16_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_2-16_-export_inference_shapes.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_2-32_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_2-32_-export_inference_shapes.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_3-32_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_3-32_-export_inference_shapes.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_loads-export_loads.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_loads-export_loads.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_matches_eager-export_match.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_matches_eager-export_match.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_static-export_static.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_static-export_static.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/export/test_decoder_export.py b/tests/export/test_decoder_export.py new file mode 100644 index 0000000..375d343 --- /dev/null +++ b/tests/export/test_decoder_export.py @@ -0,0 +1,223 @@ +from __future__ import annotations + +import pytest +import torch + +from sam3.model.data_misc import FindStage +from sam3.model.geometry_encoders import Prompt +from tests.export.utils import capture_stderr_on_fail, get_device + + +class FullInferenceWrapper(torch.nn.Module): + def __init__(self, model: torch.nn.Module): + super().__init__() + self.model = model + + def forward( + self, + images: torch.Tensor, + token_ids: torch.Tensor, + img_ids: torch.Tensor, + text_ids: torch.Tensor, + box_embeddings: torch.Tensor, + box_mask: torch.Tensor, + box_labels: torch.Tensor, + ): + model = self.model # type: ignore[assignment] + backbone_out = model.backbone.forward_image(images) + text_encoder = model.backbone.language_backbone + _, text_tokens = text_encoder.encoder(token_ids) + text_tokens = text_tokens.transpose(0, 1) + text_memory = text_encoder.resizer(text_tokens) + text_attention_mask = token_ids.ne(0) + text_attention_mask = text_attention_mask.ne(1) + backbone_out["language_features"] = text_memory + backbone_out["language_mask"] = text_attention_mask + + find_input = FindStage( + img_ids=img_ids, + text_ids=text_ids, + input_boxes=box_embeddings, + input_boxes_mask=box_mask, + input_boxes_label=box_labels, + input_points=torch.zeros( + 0, int(token_ids.shape[0]), 2, device=images.device + ), + input_points_mask=torch.zeros( + int(token_ids.shape[0]), 0, device=images.device, dtype=torch.bool + ), + ) + + geometric_prompt = Prompt( + box_embeddings=box_embeddings, + box_mask=box_mask, + box_labels=box_labels, + ) + + out = model.forward_grounding( + backbone_out=backbone_out, + find_input=find_input, + find_target=None, + geometric_prompt=geometric_prompt, + ) + + return ( + out["pred_logits"], + out["pred_boxes"], + out["pred_masks"], + out.get("presence_logit_dec"), + ) + + +def _make_inputs(batch: int, height: int, width: int, device: str): + images = torch.randn(batch, 3, height, width, device=device) + token_ids = torch.ones(batch, 32, device=device, dtype=torch.long) + token_ids[:, -1] = 0 + img_ids = torch.arange(batch, device=device, dtype=torch.long) + text_ids = torch.zeros(batch, device=device, dtype=torch.long) + box_embeddings = torch.zeros(0, batch, 4, device=device) + box_mask = torch.zeros(batch, 0, device=device, dtype=torch.bool) + box_labels = torch.zeros(0, batch, device=device, dtype=torch.long) + return images, token_ids, img_ids, text_ids, box_embeddings, box_mask, box_labels + + +def _export_decoder(model: torch.nn.Module, inputs): + ( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) = inputs + device = images.device + wrapper = FullInferenceWrapper(model).to(device).eval() # type: ignore[arg-type] + with torch.no_grad(): + exported = torch.export.export( + wrapper, + ( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ), + dynamic_shapes={ + "images": { + 0: torch.export.Dim.AUTO, + 2: 1008, + 3: 1008, + }, + "token_ids": { + 0: torch.export.Dim.AUTO, + 1: 32, + }, + "img_ids": {0: torch.export.Dim.AUTO}, + "text_ids": {0: torch.export.Dim.AUTO}, + "box_embeddings": { + 0: torch.export.Dim("num_boxes", min=0, max=32), + 1: torch.export.Dim.AUTO, + }, + "box_mask": { + 0: torch.export.Dim.AUTO, + 1: torch.export.Dim("num_boxes", min=0, max=32), + }, + "box_labels": { + 0: torch.export.Dim("num_boxes", min=0, max=32), + 1: torch.export.Dim.AUTO, + }, + }, + strict=False, + prefer_deferred_runtime_asserts_over_guards=True, + ) + return exported + + +def test_decoder_export_static(sam3_model): + device = get_device() + inputs = _make_inputs(1, 1008, 1008, device) + with capture_stderr_on_fail("export_static"): + exported = _export_decoder(sam3_model, inputs) + assert exported is not None + + +def test_decoder_export_loads(sam3_model): + device = get_device() + inputs = _make_inputs(1, 1008, 1008, device) + with capture_stderr_on_fail("export_loads"): + exported = _export_decoder(sam3_model, inputs) + module = exported.module() + with torch.no_grad(): + out = module(*inputs) + assert isinstance(out, tuple) + assert len(out) == 4 + + +def test_decoder_export_matches_eager(sam3_model): + device = get_device() + inputs = _make_inputs(1, 1008, 1008, device) + wrapper = FullInferenceWrapper(sam3_model).to(device).eval() + with torch.no_grad(): + eager_out = wrapper(*inputs) + with capture_stderr_on_fail("export_match"): + exported = _export_decoder(sam3_model, inputs) + module = exported.module() + with torch.no_grad(): + export_out = module(*inputs) + for eager, compiled in zip(eager_out, export_out): + if eager is None: + assert compiled is None + else: + torch.testing.assert_close(eager, compiled, rtol=1e-3, atol=1e-3) + + +def test_decoder_export_dynamic_batch(sam3_model): + device = get_device() + inputs = _make_inputs(1, 1008, 1008, device) + with capture_stderr_on_fail("export_dynamic_batch"): + exported = _export_decoder(sam3_model, inputs) + module = exported.module() + new_inputs = _make_inputs(2, 1008, 1008, device) + with torch.no_grad(): + out = module(*new_inputs) + assert isinstance(out, tuple) + + +def test_decoder_export_dynamic_spatial(sam3_model): + device = get_device() + inputs = _make_inputs(1, 1008, 1008, device) + with capture_stderr_on_fail("export_dynamic_spatial"): + exported = _export_decoder(sam3_model, inputs) + module = exported.module() + new_inputs = _make_inputs(1, 1008, 1008, device) + with torch.no_grad(): + out = module(*new_inputs) + assert isinstance(out, tuple) + + +def test_decoder_export_full_dynamic(sam3_model): + device = get_device() + inputs = _make_inputs(1, 1008, 1008, device) + with capture_stderr_on_fail("export_full_dynamic"): + exported = _export_decoder(sam3_model, inputs) + module = exported.module() + new_inputs = _make_inputs(3, 1008, 1008, device) + with torch.no_grad(): + out = module(*new_inputs) + assert isinstance(out, tuple) + + +@pytest.mark.parametrize("batch", [1, 2]) +def test_decoder_export_inference_shapes(sam3_model, batch: int): + device = get_device() + inputs = _make_inputs(1, 1008, 1008, device) + with capture_stderr_on_fail("export_inference_shapes"): + exported = _export_decoder(sam3_model, inputs) + module = exported.module() + new_inputs = _make_inputs(batch, 1008, 1008, device) + with torch.no_grad(): + out = module(*new_inputs) + assert isinstance(out, tuple) diff --git a/tests/export/test_encoder_export.py b/tests/export/test_encoder_export.py new file mode 100644 index 0000000..6316d2c --- /dev/null +++ b/tests/export/test_encoder_export.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import pytest +import torch + +from sam3.model.encoder import TransformerEncoderFusion +from tests.export.utils import capture_stderr_on_fail, get_device + + +class EncoderFusionWrapper(torch.nn.Module): + def __init__(self, encoder: TransformerEncoderFusion): + super().__init__() + self.encoder = encoder + + def forward( + self, + img_feats: torch.Tensor, + img_pos: torch.Tensor, + img_mask: torch.Tensor, + prompt: torch.Tensor, + prompt_mask: torch.Tensor, + ): + out = self.encoder( + src=[img_feats], + src_pos=[img_pos], + src_key_padding_mask=[img_mask], + prompt=prompt, + prompt_key_padding_mask=prompt_mask, + ) + return ( + out["memory"], + out["pos_embed"], + out["padding_mask"], + out["level_start_index"], + out["spatial_shapes"], + out["valid_ratios"], + ) + + +def _make_image_tokens(batch: int, height: int, width: int, device: str): + channels = 256 + img_feats = torch.randn(batch, channels, height, width, device=device) + img_pos = torch.randn(batch, channels, height, width, device=device) + img_mask = torch.zeros(batch, height, width, dtype=torch.bool, device=device) + return img_feats, img_pos, img_mask + + +def _make_prompt(batch: int, seq_len: int, device: str): + prompt = torch.randn(seq_len, batch, 256, device=device) + prompt_mask = torch.zeros(batch, seq_len, dtype=torch.bool, device=device) + return prompt, prompt_mask + + +def _export_encoder(model: torch.nn.Module, img_feats, img_pos, prompt, prompt_mask): + device = img_feats.device + wrapper = EncoderFusionWrapper(model.transformer.encoder).to(device).eval() # type: ignore[arg-type] + if img_feats.shape[0] == 1: + img_feats = img_feats.repeat(2, 1, 1, 1) + img_pos = img_pos.repeat(2, 1, 1, 1) + img_mask = torch.zeros( + 2, img_feats.shape[2], img_feats.shape[3], dtype=torch.bool, device=device + ) + prompt = prompt.repeat(1, 2, 1) + prompt_mask = prompt_mask.repeat(2, 1) + else: + img_mask = torch.zeros( + img_feats.shape[0], + img_feats.shape[2], + img_feats.shape[3], + dtype=torch.bool, + device=device, + ) + with torch.no_grad(): + exported = torch.export.export( + wrapper, + (img_feats, img_pos, img_mask, prompt, prompt_mask), + dynamic_shapes={ + "img_feats": { + 0: torch.export.Dim.AUTO, + }, + "img_pos": { + 0: torch.export.Dim.AUTO, + }, + "img_mask": { + 0: torch.export.Dim.AUTO, + }, + "prompt": { + 0: torch.export.Dim("seq", min=1, max=64), + 1: torch.export.Dim.AUTO, + }, + "prompt_mask": { + 0: torch.export.Dim.AUTO, + 1: torch.export.Dim("seq", min=1, max=64), + }, + }, + strict=False, + prefer_deferred_runtime_asserts_over_guards=True, + ) + return exported + + +def test_encoder_export_static(sam3_model): + device = get_device() + img_feats, img_pos, img_mask = _make_image_tokens(1, 72, 72, device) + prompt, prompt_mask = _make_prompt(1, 4, device) + with capture_stderr_on_fail("export_static"): + exported = _export_encoder(sam3_model, img_feats, img_pos, prompt, prompt_mask) + assert exported is not None + + +def test_encoder_export_loads(sam3_model): + device = get_device() + img_feats, img_pos, img_mask = _make_image_tokens(1, 72, 72, device) + prompt, prompt_mask = _make_prompt(1, 4, device) + with capture_stderr_on_fail("export_loads"): + exported = _export_encoder(sam3_model, img_feats, img_pos, prompt, prompt_mask) + module = exported.module() + with torch.no_grad(): + out = module(img_feats, img_pos, img_mask, prompt, prompt_mask) + assert isinstance(out, tuple) + assert len(out) == 6 + + +def test_encoder_export_matches_eager(sam3_model): + device = get_device() + img_feats, img_pos, img_mask = _make_image_tokens(1, 72, 72, device) + prompt, prompt_mask = _make_prompt(1, 4, device) + wrapper = EncoderFusionWrapper(sam3_model.transformer.encoder).to(device).eval() # type: ignore[arg-type] + with torch.no_grad(): + eager_out = wrapper(img_feats, img_pos, img_mask, prompt, prompt_mask) + with capture_stderr_on_fail("export_match"): + exported = _export_encoder(sam3_model, img_feats, img_pos, prompt, prompt_mask) + module = exported.module() + with torch.no_grad(): + export_out = module(img_feats, img_pos, img_mask, prompt, prompt_mask) + for eager, compiled in zip(eager_out, export_out): + torch.testing.assert_close(eager, compiled, rtol=1e-3, atol=1e-3) + + +def test_encoder_export_dynamic_batch(sam3_model): + device = get_device() + img_feats, img_pos, img_mask = _make_image_tokens(1, 72, 72, device) + prompt, prompt_mask = _make_prompt(1, 4, device) + with capture_stderr_on_fail("export_dynamic_batch"): + exported = _export_encoder(sam3_model, img_feats, img_pos, prompt, prompt_mask) + module = exported.module() + img_feats2, img_pos2, img_mask2 = _make_image_tokens(2, 72, 72, device) + prompt2, prompt_mask2 = _make_prompt(2, 4, device) + with torch.no_grad(): + out = module(img_feats2, img_pos2, img_mask2, prompt2, prompt_mask2) + assert isinstance(out, tuple) + + +def test_encoder_export_dynamic_spatial(sam3_model): + device = get_device() + img_feats, img_pos, img_mask = _make_image_tokens(1, 72, 72, device) + prompt, prompt_mask = _make_prompt(1, 4, device) + with capture_stderr_on_fail("export_dynamic_spatial"): + exported = _export_encoder(sam3_model, img_feats, img_pos, prompt, prompt_mask) + module = exported.module() + img_feats2, img_pos2, img_mask2 = _make_image_tokens(1, 72, 72, device) + with torch.no_grad(): + out = module(img_feats2, img_pos2, img_mask2, prompt, prompt_mask) + assert isinstance(out, tuple) + + +@pytest.mark.parametrize("batch,seq_len", [(1, 4), (2, 8)]) +def test_encoder_export_inference_shapes(sam3_model, batch: int, seq_len: int): + device = get_device() + img_feats, img_pos, img_mask = _make_image_tokens(1, 72, 72, device) + prompt, prompt_mask = _make_prompt(1, 4, device) + with capture_stderr_on_fail("export_inference_shapes"): + exported = _export_encoder(sam3_model, img_feats, img_pos, prompt, prompt_mask) + module = exported.module() + img_feats2, img_pos2, img_mask2 = _make_image_tokens(batch, 72, 72, device) + prompt2, prompt_mask2 = _make_prompt(batch, seq_len, device) + with torch.no_grad(): + out = module(img_feats2, img_pos2, img_mask2, prompt2, prompt_mask2) + assert isinstance(out, tuple) diff --git a/tests/export/test_image_encoder_export.py b/tests/export/test_image_encoder_export.py new file mode 100644 index 0000000..7de72c1 --- /dev/null +++ b/tests/export/test_image_encoder_export.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import pytest +import torch + +from sam3.model.vl_combiner import SAM3VLBackbone +from tests.export.utils import capture_stderr_on_fail, get_device + + +class ImageEncoderWrapper(torch.nn.Module): + def __init__(self, backbone: SAM3VLBackbone): + super().__init__() + self.backbone = backbone + + def forward(self, images: torch.Tensor): + out = self.backbone._forward_image_no_act_ckpt(images) + return ( + out["vision_features"], + out["vision_pos_enc"], + out["backbone_fpn"], + ) + + +def _make_images(batch: int, height: int, width: int, device: str) -> torch.Tensor: + return torch.randn(batch, 3, height, width, device=device, dtype=torch.float32) + + +def _export_image_encoder(model: torch.nn.Module, images: torch.Tensor): + device = images.device + wrapper = ImageEncoderWrapper(model.backbone).to(device).eval() # type: ignore[arg-type] + export_images = images + if images.shape[0] == 1: + export_images = images.repeat(2, 1, 1, 1) + with torch.no_grad(): + height = torch.export.Dim("height", min=3, max=4) + width = torch.export.Dim("width", min=3, max=4) + exported = torch.export.export( + wrapper, + (export_images,), + dynamic_shapes={ + "images": { + 0: torch.export.Dim("batch", min=1, max=4), + 2: 336 * height, + 3: 336 * width, + } + }, + strict=False, + prefer_deferred_runtime_asserts_over_guards=True, + ) + return exported + + +def test_image_encoder_export_static(sam3_model): + device = get_device() + images = _make_images(1, 1008, 1008, device) + with capture_stderr_on_fail("export_static"): + exported = _export_image_encoder(sam3_model, images) + assert exported is not None + + +def test_image_encoder_export_loads(sam3_model): + device = get_device() + images = _make_images(1, 1008, 1008, device) + with capture_stderr_on_fail("export_loads"): + exported = _export_image_encoder(sam3_model, images) + module = exported.module() + with torch.no_grad(): + out = module(images) + assert isinstance(out, tuple) + assert len(out) == 3 + + +def test_image_encoder_export_matches_eager(sam3_model): + device = get_device() + images = _make_images(1, 1008, 1008, device) + wrapper = ImageEncoderWrapper(sam3_model.backbone).to(device).eval() + with torch.no_grad(): + eager_out = wrapper(images) + with capture_stderr_on_fail("export_match"): + exported = _export_image_encoder(sam3_model, images) + module = exported.module() + with torch.no_grad(): + export_out = module(images) + for eager, compiled in zip(eager_out, export_out): + if isinstance(eager, (list, tuple)): + for e_item, c_item in zip(eager, compiled): + torch.testing.assert_close(e_item, c_item, rtol=1e-3, atol=1e-3) + else: + torch.testing.assert_close(eager, compiled, rtol=1e-3, atol=1e-3) + + +def test_image_encoder_export_dynamic_batch(sam3_model): + device = get_device() + images = _make_images(1, 1008, 1008, device) + with capture_stderr_on_fail("export_dynamic_batch"): + exported = _export_image_encoder(sam3_model, images) + module = exported.module() + with torch.no_grad(): + out = module(_make_images(2, 1008, 1008, device)) + assert isinstance(out, tuple) + + +@pytest.mark.parametrize("height,width", [(1008, 1008)]) +def test_image_encoder_export_dynamic_spatial(sam3_model, height: int, width: int): + device = get_device() + images = _make_images(1, 1008, 1008, device) + with capture_stderr_on_fail("export_dynamic_spatial"): + exported = _export_image_encoder(sam3_model, images) + module = exported.module() + with torch.no_grad(): + out = module(_make_images(1, height, width, device)) + assert isinstance(out, tuple) + + +def test_image_encoder_export_full_dynamic(sam3_model): + device = get_device() + images = _make_images(1, 1008, 1008, device) + with capture_stderr_on_fail("export_full_dynamic"): + exported = _export_image_encoder(sam3_model, images) + module = exported.module() + with torch.no_grad(): + out = module(_make_images(3, 1008, 1008, device)) + assert isinstance(out, tuple) + + +@pytest.mark.parametrize("batch,height,width", [(2, 1008, 1008)]) +def test_image_encoder_export_inference_shapes( + sam3_model, batch: int, height: int, width: int +): + device = get_device() + images = _make_images(1, 1008, 1008, device) + with capture_stderr_on_fail("export_inference_shapes"): + exported = _export_image_encoder(sam3_model, images) + module = exported.module() + with torch.no_grad(): + out = module(_make_images(batch, height, width, device)) + assert isinstance(out, tuple) diff --git a/tests/export/test_text_encoder_export.py b/tests/export/test_text_encoder_export.py new file mode 100644 index 0000000..402ba8c --- /dev/null +++ b/tests/export/test_text_encoder_export.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import pytest +import torch + +from sam3.model.text_encoder_ve import VETextEncoder +from tests.export.utils import capture_stderr_on_fail, get_device + + +class TextEncoderWrapper(torch.nn.Module): + def __init__(self, text_encoder: VETextEncoder): + super().__init__() + self.text_encoder = text_encoder + + def forward(self, token_ids: torch.Tensor): + _, text_tokens = self.text_encoder.encoder(token_ids) + text_tokens = text_tokens.transpose(0, 1) + text_memory = self.text_encoder.resizer(text_tokens) + text_attention_mask = token_ids.ne(0) + text_attention_mask = text_attention_mask.ne(1) + return text_attention_mask, text_memory + + +def _make_tokens(batch: int, seq_len: int, vocab_size: int, device: str) -> torch.Tensor: + token_ids = torch.randint(0, vocab_size, (batch, seq_len), device=device) + token_ids[:, -1] = 1 + return token_ids + + +def _export_text_encoder(model: torch.nn.Module, token_ids: torch.Tensor): + device = token_ids.device + wrapper = TextEncoderWrapper(model.backbone.language_backbone).to(device).eval() # type: ignore[arg-type] + export_tokens = token_ids + if token_ids.shape[0] == 1: + export_tokens = token_ids.repeat(2, 1) + with torch.no_grad(): + exported = torch.export.export( + wrapper, + (export_tokens,), + dynamic_shapes={ + "token_ids": { + 0: torch.export.Dim("batch", min=1, max=4), + 1: torch.export.Dim.AUTO, + } + }, + strict=False, + prefer_deferred_runtime_asserts_over_guards=True, + ) + return exported + + +def test_text_encoder_export_static(sam3_model): + device = get_device() + vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size + token_ids = _make_tokens(1, 32, vocab_size, device) + with capture_stderr_on_fail("export_static"): + exported = _export_text_encoder(sam3_model, token_ids) + assert exported is not None + + +def test_text_encoder_export_loads(sam3_model): + device = get_device() + vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size + token_ids = _make_tokens(1, 32, vocab_size, device) + with capture_stderr_on_fail("export_loads"): + exported = _export_text_encoder(sam3_model, token_ids) + module = exported.module() + with torch.no_grad(): + out = module(token_ids) + assert isinstance(out, tuple) + assert len(out) == 2 + + +def test_text_encoder_export_matches_eager(sam3_model): + device = get_device() + vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size + token_ids = _make_tokens(1, 32, vocab_size, device) + wrapper = TextEncoderWrapper(sam3_model.backbone.language_backbone).to(device).eval() # type: ignore[arg-type] + with torch.no_grad(): + eager_out = wrapper(token_ids) + with capture_stderr_on_fail("export_match"): + exported = _export_text_encoder(sam3_model, token_ids) + module = exported.module() + with torch.no_grad(): + export_out = module(token_ids) + torch.testing.assert_close(eager_out[0], export_out[0]) + torch.testing.assert_close(eager_out[1], export_out[1], rtol=1e-3, atol=1e-3) + + +def test_text_encoder_export_dynamic_batch(sam3_model): + device = get_device() + vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size + token_ids = _make_tokens(1, 32, vocab_size, device) + with capture_stderr_on_fail("export_dynamic_batch"): + exported = _export_text_encoder(sam3_model, token_ids) + module = exported.module() + with torch.no_grad(): + out = module(_make_tokens(2, 32, vocab_size, device)) + assert isinstance(out, tuple) + + +@pytest.mark.parametrize("seq_len", [32]) +def test_text_encoder_export_dynamic_seq_len(sam3_model, seq_len: int): + device = get_device() + vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size + token_ids = _make_tokens(1, 32, vocab_size, device) + with capture_stderr_on_fail("export_dynamic_seq_len"): + exported = _export_text_encoder(sam3_model, token_ids) + module = exported.module() + with torch.no_grad(): + out = module(_make_tokens(1, seq_len, vocab_size, device)) + assert isinstance(out, tuple) + + +@pytest.mark.parametrize("batch,seq_len", [(1, 32), (2, 32)]) +def test_text_encoder_export_inference_shapes( + sam3_model, batch: int, seq_len: int +): + device = get_device() + vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size + token_ids = _make_tokens(1, 32, vocab_size, device) + with capture_stderr_on_fail("export_inference_shapes"): + exported = _export_text_encoder(sam3_model, token_ids) + module = exported.module() + with torch.no_grad(): + out = module(_make_tokens(batch, seq_len, vocab_size, device)) + assert isinstance(out, tuple) diff --git a/tests/export/utils.py b/tests/export/utils.py new file mode 100644 index 0000000..8bff9a3 --- /dev/null +++ b/tests/export/utils.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import io +import os +import re +from contextlib import contextmanager, redirect_stderr +from typing import Generator +from pathlib import Path + +import torch + +LOG_DIR = Path(__file__).resolve().parent / "export_logs" + + +def _sanitize_test_name(name: str) -> str: + return re.sub(r"[^A-Za-z0-9_.-]+", "_", name) + + +def _current_test_name() -> str: + raw = os.getenv("PYTEST_CURRENT_TEST", "unknown") + return _sanitize_test_name(raw.split(" ")[0]) + + +@contextmanager +def capture_stderr_on_fail(suffix: str) -> Generator[None, None, None]: + LOG_DIR.mkdir(parents=True, exist_ok=True) + buffer = io.StringIO() + with redirect_stderr(buffer): + try: + yield + except Exception: + log_path = LOG_DIR / f"{_current_test_name()}-{suffix}.stderr.txt" + with log_path.open("w", encoding="utf-8") as handle: + handle.write(buffer.getvalue()) + raise + + +def get_device() -> str: + force_cpu = os.getenv("SAM3_EXPORT_FORCE_CPU", "0") == "1" + device = os.getenv("SAM3_EXPORT_DEVICE") + if device is None: + device = "cuda" if torch.cuda.is_available() and not force_cpu else "cpu" + return device From 3d5e8d4a125efc4cfbe21ea9e073b8e65accc7a1 Mon Sep 17 00:00:00 2001 From: Ryan Avery Date: Thu, 5 Feb 2026 09:30:44 -0800 Subject: [PATCH 04/31] Add export artifact scripts and update export fixes --- assets/images/cat_dog.jpg | Bin 0 -> 6022 bytes pyproject.toml | 21 +- sam3/model/decoder.py | 89 +- sam3/model/geometry_encoders.py | 9 +- scripts/export_sam3_artifacts.py | 157 + scripts/test_sam3_artifacts.py | 273 + tests/export/test_decoder_export.py | 46 +- tests/export/test_image_encoder_export.py | 6 +- uv.lock | 5825 +++++++++++++++++++++ 9 files changed, 6384 insertions(+), 42 deletions(-) create mode 100644 assets/images/cat_dog.jpg create mode 100644 scripts/export_sam3_artifacts.py create mode 100644 scripts/test_sam3_artifacts.py create mode 100644 uv.lock diff --git a/assets/images/cat_dog.jpg b/assets/images/cat_dog.jpg new file mode 100644 index 0000000000000000000000000000000000000000..03687692402bbb6ace6ef816485fdc7a5636115e GIT binary patch literal 6022 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.1.10", "typing_extensions", "huggingface_hub", + "einops", + "psutil", ] [project.optional-dependencies] @@ -92,6 +94,23 @@ sam3 = ["assets/*.txt.gz"] [tool.setuptools.dynamic] version = {attr = "sam3.__version__"} +[dependency-groups] +dev = [ + "pytest", + "pytest-cov", + "black==24.2.0", + "ufmt==2.8.0", + "ruff-api==0.1.0", + "usort==1.0.2", + "gitpython==3.1.31", + "yt-dlp", + "pandas", + "opencv-python", + "pycocotools", + "numba", + "python-rapidjson", +] + [tool.black] line-length = 88 target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] diff --git a/sam3/model/decoder.py b/sam3/model/decoder.py index 4ecfe91..7555397 100644 --- a/sam3/model/decoder.py +++ b/sam3/model/decoder.py @@ -10,6 +10,7 @@ import numpy as np import torch +import torch.nn.functional as F from sam3.sam.transformer import RoPEAttention from torch import nn, Tensor from torchvision.ops.roi_align import RoIAlign @@ -152,23 +153,39 @@ def forward( tgt = self.catext_norm(tgt) if presence_token is not None and cross_attn_mask is not None: - presence_token_mask = torch.zeros_like(cross_attn_mask[:, :1, :]) - cross_attn_mask = torch.cat( - [presence_token_mask, cross_attn_mask], dim=1 - ) # (bs*nheads, 1+nq, hw) + if cross_attn_mask.dim() == 4: + presence_token_mask = torch.zeros_like(cross_attn_mask[:, :, :1, :]) + cross_attn_mask = torch.cat( + [presence_token_mask, cross_attn_mask], dim=2 + ) # (bs, nheads, 1+nq, hw) + else: + presence_token_mask = torch.zeros_like(cross_attn_mask[:, :1, :]) + cross_attn_mask = torch.cat( + [presence_token_mask, cross_attn_mask], dim=1 + ) # (bs*nheads, 1+nq, hw) # Cross attention to image - tgt2 = self.cross_attn( - query=self.with_pos_embed(tgt, tgt_query_pos), - key=self.with_pos_embed(memory, memory_pos), - value=memory, - attn_mask=cross_attn_mask, - key_padding_mask=( - memory_key_padding_mask.transpose(0, 1) - if memory_key_padding_mask is not None - else None - ), - )[0] + key_padding_mask = ( + memory_key_padding_mask.transpose(0, 1) + if memory_key_padding_mask is not None + else None + ) + if cross_attn_mask is not None and cross_attn_mask.dim() == 4: + tgt2 = self._cross_attn_with_rpb( + query=self.with_pos_embed(tgt, tgt_query_pos), + key=self.with_pos_embed(memory, memory_pos), + value=memory, + attn_bias=cross_attn_mask, + key_padding_mask=key_padding_mask, + ) + else: + tgt2 = self.cross_attn( + query=self.with_pos_embed(tgt, tgt_query_pos), + key=self.with_pos_embed(memory, memory_pos), + value=memory, + attn_mask=cross_attn_mask, + key_padding_mask=key_padding_mask, + )[0] tgt = tgt + self.dropout1(tgt2) tgt = self.norm1(tgt) @@ -183,6 +200,44 @@ def forward( return tgt, presence_token_out + def _cross_attn_with_rpb( + self, + query: Tensor, + key: Tensor, + value: Tensor, + attn_bias: Tensor, + key_padding_mask: Optional[Tensor], + ) -> Tensor: + mha = self.cross_attn + assert isinstance(mha, nn.MultiheadAttention) + q, k, v = F._in_projection_packed( + query, key, value, mha.in_proj_weight, mha.in_proj_bias + ) + tgt_len, bsz, _ = q.shape + num_heads = mha.num_heads + head_dim = mha.head_dim + q = q.contiguous().view(tgt_len, bsz, num_heads, head_dim).permute(1, 2, 0, 3) + k = k.contiguous().view(-1, bsz, num_heads, head_dim).permute(1, 2, 0, 3) + v = v.contiguous().view(-1, bsz, num_heads, head_dim).permute(1, 2, 0, 3) + src_len = k.shape[2] + bias = attn_bias + if bias.dim() == 3: + bias = bias.view(bsz, num_heads, tgt_len, src_len) + if key_padding_mask is not None: + pad = key_padding_mask[:, None, None, :].to(dtype=q.dtype) + pad = pad.masked_fill(pad > 0, float("-inf")) + bias = pad if bias is None else bias + pad + attn_output = F.scaled_dot_product_attention( + q, + k, + v, + attn_mask=bias, + dropout_p=mha.dropout if self.training else 0.0, + is_causal=False, + ) + attn_output = attn_output.permute(2, 0, 1, 3).reshape(tgt_len, bsz, -1) + return F.linear(attn_output, mha.out_proj.weight, mha.out_proj.bias) + class TransformerDecoder(nn.Module): def __init__( @@ -498,9 +553,8 @@ def forward( # conditional query query_pos = self.ref_point_head(query_sine_embed) # nq, bs, d_model - compiling = torch.compiler.is_dynamo_compiling() or torch._dynamo.is_compiling() memory_mask = None - if self.boxRPB != "none" and reference_boxes is not None and not compiling: + if self.boxRPB != "none" and reference_boxes is not None: assert spatial_shapes.shape[0] == 1, ( "only single scale support implemented" ) @@ -508,7 +562,6 @@ def forward( reference_boxes, (spatial_shapes[0, 0], spatial_shapes[0, 1]), ) - memory_mask = memory_mask.flatten(0, 1) # (bs*n_heads, nq, H*W) if self.training: assert self.use_act_checkpoint, ( "Activation checkpointing not enabled in the decoder" diff --git a/sam3/model/geometry_encoders.py b/sam3/model/geometry_encoders.py index fda880e..815d0d3 100644 --- a/sam3/model/geometry_encoders.py +++ b/sam3/model/geometry_encoders.py @@ -658,8 +658,15 @@ def _encode_boxes(self, boxes, boxes_mask, boxes_labels, img_feats): ) scale = scale.view(1, 1, 4) boxes_xyxy = boxes_xyxy * scale + boxes_xyxy = boxes_xyxy.transpose(0, 1) + batch_idx = torch.arange( + bs, device=boxes_xyxy.device, dtype=boxes_xyxy.dtype + ) + batch_idx = batch_idx.view(bs, 1, 1).expand(bs, n_boxes, 1) + boxes_for_roi = torch.cat([batch_idx, boxes_xyxy], dim=-1) + boxes_for_roi = boxes_for_roi.reshape(-1, 5) sampled = torchvision.ops.roi_align( - img_feats, boxes_xyxy.float().transpose(0, 1).unbind(0), self.roi_size + img_feats, boxes_for_roi.float(), self.roi_size ) assert list(sampled.shape) == [ bs * n_boxes, diff --git a/scripts/export_sam3_artifacts.py b/scripts/export_sam3_artifacts.py new file mode 100644 index 0000000..bf4a34f --- /dev/null +++ b/scripts/export_sam3_artifacts.py @@ -0,0 +1,157 @@ +import argparse +import sys +from pathlib import Path + +import numpy as np +import torch +from PIL import Image + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT)) + +from sam3.model_builder import build_sam3_image_model +from tests.export.test_decoder_export import _export_decoder +from tests.export.test_encoder_export import EncoderFusionWrapper +from tests.export.test_image_encoder_export import _export_image_encoder +from tests.export.test_text_encoder_export import _export_text_encoder + + +def _load_image(path: Path, device: torch.device) -> torch.Tensor: + image = Image.open(path).convert("RGB") + np_image = np.array(image, dtype=np.float32) / 255.0 + tensor = torch.from_numpy(np_image).permute(2, 0, 1).unsqueeze(0) + return tensor.to(device) + + +def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: + image = image.clamp(0, 1) + image = torch.nn.functional.interpolate( + image, size=(size, size), mode="bilinear", align_corners=False + ) + mean = torch.tensor([0.5, 0.5, 0.5], device=image.device).view(1, 3, 1, 1) + std = torch.tensor([0.5, 0.5, 0.5], device=image.device).view(1, 3, 1, 1) + return (image - mean) / std + + +def _make_inputs(model, image: torch.Tensor, prompts): + device = image.device + num_prompts = len(prompts) + + tokenizer = model.backbone.language_backbone.tokenizer + token_ids = tokenizer(prompts, context_length=32).to(device) + + img_ids = torch.zeros(num_prompts, device=device, dtype=torch.long) + text_ids = torch.zeros(num_prompts, device=device, dtype=torch.long) + + box_embeddings = torch.zeros(1, num_prompts, 4, device=device) + box_mask = torch.zeros(num_prompts, 1, device=device, dtype=torch.bool) + box_labels = torch.zeros(1, num_prompts, device=device, dtype=torch.long) + + return ( + image, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) + + +def _save_export(exported, path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + exported.save(str(path)) + + +def _export_encoder_multilevel( + model, img_feats, img_pos, img_mask, prompt, prompt_mask +): + device = prompt.device + wrapper = EncoderFusionWrapper(model.transformer.encoder).to(device).eval() + with torch.no_grad(): + exported = torch.export.export( + wrapper, + (img_feats, img_pos, img_mask, prompt, prompt_mask), + strict=False, + prefer_deferred_runtime_asserts_over_guards=True, + ) + return exported + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--image", + type=Path, + default=Path("assets/images/cat_dog.jpg"), + help="Path to input image", + ) + parser.add_argument( + "--prompts", + type=str, + default="cat,dog", + help="Comma-separated text prompts", + ) + parser.add_argument( + "--device", + type=str, + default="cuda" if torch.cuda.is_available() else "cpu", + ) + parser.add_argument( + "--out-dir", + type=Path, + default=Path("artifacts/export"), + help="Directory to write exported artifacts", + ) + args = parser.parse_args() + + prompts = [p.strip() for p in args.prompts.split(",") if p.strip()] + if not prompts: + raise ValueError("Provide at least one prompt") + + model = build_sam3_image_model( + device=args.device, eval_mode=True, enable_segmentation=True + ) + model.eval() + + image = _load_image(args.image, torch.device(args.device)) + image = _prepare_image(image, size=1008) + inputs = _make_inputs(model, image, prompts) + + print("Exporting image encoder...") + image_encoder = _export_image_encoder(model, inputs[0]) + print("Exporting text encoder...") + text_encoder = _export_text_encoder(model, inputs[1]) + print("Exporting encoder fusion...") + with torch.no_grad(): + image_module = image_encoder.module() + text_module = text_encoder.module() + vision_features, vision_pos_enc, _ = image_module(inputs[0]) + text_attention_mask, text_memory = text_module(inputs[1]) + prompt = text_memory + prompt_mask = text_attention_mask + img_mask = [ + torch.zeros( + feat.shape[0], + feat.shape[2], + feat.shape[3], + device=feat.device, + dtype=torch.bool, + ) + for feat in vision_features + ] + encoder = _export_encoder_multilevel( + model, vision_features, vision_pos_enc, img_mask, prompt, prompt_mask + ) + print("Exporting decoder...") + decoder = _export_decoder(model, inputs) + + _save_export(image_encoder, args.out_dir / "image_encoder.pt2") + _save_export(text_encoder, args.out_dir / "text_encoder.pt2") + _save_export(encoder, args.out_dir / "encoder_fusion.pt2") + _save_export(decoder, args.out_dir / "decoder.pt2") + print("Saved exports to", args.out_dir) + + +if __name__ == "__main__": + main() diff --git a/scripts/test_sam3_artifacts.py b/scripts/test_sam3_artifacts.py new file mode 100644 index 0000000..dc83448 --- /dev/null +++ b/scripts/test_sam3_artifacts.py @@ -0,0 +1,273 @@ +import argparse +import sys +from pathlib import Path + +import numpy as np +import torch +from PIL import Image +from PIL import ImageDraw + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT)) + +from sam3.model_builder import build_sam3_image_model +from sam3.model.data_misc import FindStage +from sam3.model.geometry_encoders import Prompt + + +def _load_image(path: Path, device: torch.device) -> torch.Tensor: + image = Image.open(path).convert("RGB") + np_image = np.array(image, dtype=np.float32) / 255.0 + tensor = torch.from_numpy(np_image).permute(2, 0, 1).unsqueeze(0) + return tensor.to(device) + + +def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: + image = image.clamp(0, 1) + image = torch.nn.functional.interpolate( + image, size=(size, size), mode="bilinear", align_corners=False + ) + mean = torch.tensor([0.5, 0.5, 0.5], device=image.device).view(1, 3, 1, 1) + std = torch.tensor([0.5, 0.5, 0.5], device=image.device).view(1, 3, 1, 1) + return (image - mean) / std + + +def _make_inputs(model, image: torch.Tensor, prompts): + device = image.device + num_prompts = len(prompts) + + tokenizer = model.backbone.language_backbone.tokenizer + token_ids = tokenizer(prompts, context_length=32).to(device) + + img_ids = torch.zeros(num_prompts, device=device, dtype=torch.long) + text_ids = torch.zeros(num_prompts, device=device, dtype=torch.long) + + box_embeddings = torch.zeros(1, num_prompts, 4, device=device) + box_mask = torch.zeros(num_prompts, 1, device=device, dtype=torch.bool) + box_labels = torch.zeros(1, num_prompts, device=device, dtype=torch.long) + + return ( + image, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) + + +def _run_full_model(model, inputs): + ( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) = inputs + backbone_out = model.backbone.forward_image(images) + text_encoder = model.backbone.language_backbone + _, text_tokens = text_encoder.encoder(token_ids) + text_tokens = text_tokens.transpose(0, 1) + text_memory = text_encoder.resizer(text_tokens) + text_attention_mask = token_ids.ne(0) + text_attention_mask = text_attention_mask.ne(1) + backbone_out["language_features"] = text_memory + backbone_out["language_mask"] = text_attention_mask + + find_input = FindStage( + img_ids=img_ids, + text_ids=text_ids, + input_boxes=box_embeddings, + input_boxes_mask=box_mask, + input_boxes_label=box_labels, + input_points=torch.zeros(0, int(token_ids.shape[0]), 2, device=images.device), + input_points_mask=torch.zeros( + int(token_ids.shape[0]), 0, device=images.device, dtype=torch.bool + ), + ) + geometric_prompt = Prompt( + box_embeddings=box_embeddings, + box_mask=box_mask, + box_labels=box_labels, + ) + out = model.forward_grounding( + backbone_out=backbone_out, + find_input=find_input, + find_target=None, + geometric_prompt=geometric_prompt, + ) + return ( + out["pred_masks"], + out["pred_boxes"], + out["pred_logits"], + out["pred_boxes_xyxy"], + ) + + +def _load_export(path: Path): + exported = torch.export.load(str(path)) + return exported.module() + + +def _to_pil_image(image: torch.Tensor) -> Image.Image: + image = image.detach().cpu().clamp(0, 1) + np_image = (image.permute(1, 2, 0).numpy() * 255.0).astype(np.uint8) + return Image.fromarray(np_image) + + +def _color_palette(num_colors: int): + base = [ + (255, 99, 71), + (65, 105, 225), + (60, 179, 113), + (238, 130, 238), + (255, 215, 0), + (255, 165, 0), + ] + return [base[i % len(base)] for i in range(num_colors)] + + +def _overlay_masks( + image: Image.Image, masks: torch.Tensor, scores: torch.Tensor, out_path: Path +): + num_prompts, num_queries = scores.shape[:2] + best_idx = scores.squeeze(-1).argmax(dim=1) + colors = _color_palette(num_prompts) + base = image.copy() + overlay = Image.new("RGBA", base.size, (0, 0, 0, 0)) + for i in range(num_prompts): + mask = masks[i, best_idx[i]].detach().cpu() + mask = mask > 0 + mask_img = Image.fromarray((mask.numpy() * 255).astype(np.uint8), mode="L") + color = colors[i] + color_img = Image.new("RGBA", base.size, (*color, 120)) + overlay = Image.composite(color_img, overlay, mask_img) + blended = Image.alpha_composite(base.convert("RGBA"), overlay) + blended.convert("RGB").save(out_path) + + +def _draw_boxes( + image: Image.Image, boxes_xyxy: torch.Tensor, scores: torch.Tensor, out_path: Path +): + num_prompts, num_queries = scores.shape[:2] + best_idx = scores.squeeze(-1).argmax(dim=1) + colors = _color_palette(num_prompts) + draw = ImageDraw.Draw(image) + for i in range(num_prompts): + box = boxes_xyxy[i, best_idx[i]].detach().cpu().tolist() + color = colors[i] + draw.rectangle(box, outline=color, width=3) + image.save(out_path) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--image", + type=Path, + default=Path("assets/images/cat_dog.jpg"), + help="Path to input image", + ) + parser.add_argument( + "--prompts", + type=str, + default="cat,dog", + help="Comma-separated text prompts", + ) + parser.add_argument( + "--device", + type=str, + default="cuda" if torch.cuda.is_available() else "cpu", + ) + parser.add_argument( + "--artifact-dir", + type=Path, + default=Path("artifacts/export"), + help="Directory with exported artifacts", + ) + args = parser.parse_args() + + prompts = [p.strip() for p in args.prompts.split(",") if p.strip()] + if not prompts: + raise ValueError("Provide at least one prompt") + + model = build_sam3_image_model( + device=args.device, eval_mode=True, enable_segmentation=True + ) + model.eval() + + image = _load_image(args.image, torch.device(args.device)) + image = _prepare_image(image, size=1008) + inputs = _make_inputs(model, image, prompts) + + with torch.no_grad(): + eager_masks, eager_boxes, eager_logits, eager_boxes_xyxy = _run_full_model( + model, inputs + ) + + image_module = _load_export(args.artifact_dir / "image_encoder.pt2") + text_module = _load_export(args.artifact_dir / "text_encoder.pt2") + encoder_module = _load_export(args.artifact_dir / "encoder_fusion.pt2") + decoder_module = _load_export(args.artifact_dir / "decoder.pt2") + + with torch.no_grad(): + vision_features, vision_pos_enc, _ = image_module(inputs[0]) + text_attention_mask, text_memory = text_module(inputs[1]) + img_mask = [ + torch.zeros( + feat.shape[0], + feat.shape[2], + feat.shape[3], + device=feat.device, + dtype=torch.bool, + ) + for feat in vision_features + ] + enc_out = encoder_module( + vision_features, vision_pos_enc, img_mask, text_memory, text_attention_mask + ) + assert isinstance(enc_out, tuple) + pred_logits, pred_boxes, pred_masks, pred_boxes_xyxy = decoder_module(*inputs) + + print("Prompt count:", len(prompts)) + print("Pred logits shape:", pred_logits.shape) + print("Pred boxes shape:", pred_boxes.shape) + print("Pred masks shape:", pred_masks.shape) + torch.testing.assert_close(pred_logits, eager_logits, rtol=1e-3, atol=1e-3) + print("Eager vs export logits match") + + out_dir = args.artifact_dir + out_dir.mkdir(parents=True, exist_ok=True) + base_image = _to_pil_image(inputs[0][0]) + _overlay_masks( + base_image.copy(), + eager_masks, + eager_logits, + out_dir / "eager_masks_overlay.jpg", + ) + _overlay_masks( + base_image.copy(), + pred_masks, + pred_logits, + out_dir / "export_masks_overlay.jpg", + ) + _draw_boxes( + base_image.copy(), + eager_boxes_xyxy, + eager_logits, + out_dir / "eager_boxes_overlay.jpg", + ) + _draw_boxes( + base_image.copy(), + pred_boxes_xyxy, + pred_logits, + out_dir / "export_boxes_overlay.jpg", + ) + print("Saved overlays to", out_dir) + + +if __name__ == "__main__": + main() diff --git a/tests/export/test_decoder_export.py b/tests/export/test_decoder_export.py index 375d343..e3680d3 100644 --- a/tests/export/test_decoder_export.py +++ b/tests/export/test_decoder_export.py @@ -69,15 +69,17 @@ def forward( ) -def _make_inputs(batch: int, height: int, width: int, device: str): +def _make_inputs(batch: int, height: int, width: int, device: str, num_boxes: int = 0): images = torch.randn(batch, 3, height, width, device=device) token_ids = torch.ones(batch, 32, device=device, dtype=torch.long) token_ids[:, -1] = 0 img_ids = torch.arange(batch, device=device, dtype=torch.long) text_ids = torch.zeros(batch, device=device, dtype=torch.long) - box_embeddings = torch.zeros(0, batch, 4, device=device) - box_mask = torch.zeros(batch, 0, device=device, dtype=torch.bool) - box_labels = torch.zeros(0, batch, device=device, dtype=torch.long) + box_embeddings = torch.rand(num_boxes, batch, 4, device=device) + if num_boxes > 0: + box_embeddings[..., 2:] = box_embeddings[..., 2:] * 0.5 + box_mask = torch.zeros(batch, num_boxes, device=device, dtype=torch.bool) + box_labels = torch.zeros(num_boxes, batch, device=device, dtype=torch.long) return images, token_ids, img_ids, text_ids, box_embeddings, box_mask, box_labels @@ -93,6 +95,14 @@ def _export_decoder(model: torch.nn.Module, inputs): ) = inputs device = images.device wrapper = FullInferenceWrapper(model).to(device).eval() # type: ignore[arg-type] + if images.shape[0] == 1: + images = images.repeat(2, 1, 1, 1) + token_ids = token_ids.repeat(2, 1) + img_ids = img_ids.repeat(2) + text_ids = text_ids.repeat(2) + box_embeddings = box_embeddings.repeat(1, 2, 1) + box_mask = box_mask.repeat(2, 1) + box_labels = box_labels.repeat(1, 2) with torch.no_grad(): exported = torch.export.export( wrapper, @@ -118,15 +128,15 @@ def _export_decoder(model: torch.nn.Module, inputs): "img_ids": {0: torch.export.Dim.AUTO}, "text_ids": {0: torch.export.Dim.AUTO}, "box_embeddings": { - 0: torch.export.Dim("num_boxes", min=0, max=32), + 0: 1, 1: torch.export.Dim.AUTO, }, "box_mask": { 0: torch.export.Dim.AUTO, - 1: torch.export.Dim("num_boxes", min=0, max=32), + 1: 1, }, "box_labels": { - 0: torch.export.Dim("num_boxes", min=0, max=32), + 0: 1, 1: torch.export.Dim.AUTO, }, }, @@ -138,7 +148,7 @@ def _export_decoder(model: torch.nn.Module, inputs): def test_decoder_export_static(sam3_model): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device) + inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) with capture_stderr_on_fail("export_static"): exported = _export_decoder(sam3_model, inputs) assert exported is not None @@ -146,7 +156,7 @@ def test_decoder_export_static(sam3_model): def test_decoder_export_loads(sam3_model): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device) + inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) with capture_stderr_on_fail("export_loads"): exported = _export_decoder(sam3_model, inputs) module = exported.module() @@ -158,7 +168,7 @@ def test_decoder_export_loads(sam3_model): def test_decoder_export_matches_eager(sam3_model): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device) + inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) wrapper = FullInferenceWrapper(sam3_model).to(device).eval() with torch.no_grad(): eager_out = wrapper(*inputs) @@ -176,11 +186,11 @@ def test_decoder_export_matches_eager(sam3_model): def test_decoder_export_dynamic_batch(sam3_model): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device) + inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) with capture_stderr_on_fail("export_dynamic_batch"): exported = _export_decoder(sam3_model, inputs) module = exported.module() - new_inputs = _make_inputs(2, 1008, 1008, device) + new_inputs = _make_inputs(2, 1008, 1008, device, num_boxes=1) with torch.no_grad(): out = module(*new_inputs) assert isinstance(out, tuple) @@ -188,11 +198,11 @@ def test_decoder_export_dynamic_batch(sam3_model): def test_decoder_export_dynamic_spatial(sam3_model): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device) + inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) with capture_stderr_on_fail("export_dynamic_spatial"): exported = _export_decoder(sam3_model, inputs) module = exported.module() - new_inputs = _make_inputs(1, 1008, 1008, device) + new_inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) with torch.no_grad(): out = module(*new_inputs) assert isinstance(out, tuple) @@ -200,11 +210,11 @@ def test_decoder_export_dynamic_spatial(sam3_model): def test_decoder_export_full_dynamic(sam3_model): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device) + inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) with capture_stderr_on_fail("export_full_dynamic"): exported = _export_decoder(sam3_model, inputs) module = exported.module() - new_inputs = _make_inputs(3, 1008, 1008, device) + new_inputs = _make_inputs(2, 1008, 1008, device, num_boxes=1) with torch.no_grad(): out = module(*new_inputs) assert isinstance(out, tuple) @@ -213,11 +223,11 @@ def test_decoder_export_full_dynamic(sam3_model): @pytest.mark.parametrize("batch", [1, 2]) def test_decoder_export_inference_shapes(sam3_model, batch: int): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device) + inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) with capture_stderr_on_fail("export_inference_shapes"): exported = _export_decoder(sam3_model, inputs) module = exported.module() - new_inputs = _make_inputs(batch, 1008, 1008, device) + new_inputs = _make_inputs(batch, 1008, 1008, device, num_boxes=1) with torch.no_grad(): out = module(*new_inputs) assert isinstance(out, tuple) diff --git a/tests/export/test_image_encoder_export.py b/tests/export/test_image_encoder_export.py index 7de72c1..58aa4fb 100644 --- a/tests/export/test_image_encoder_export.py +++ b/tests/export/test_image_encoder_export.py @@ -32,16 +32,14 @@ def _export_image_encoder(model: torch.nn.Module, images: torch.Tensor): if images.shape[0] == 1: export_images = images.repeat(2, 1, 1, 1) with torch.no_grad(): - height = torch.export.Dim("height", min=3, max=4) - width = torch.export.Dim("width", min=3, max=4) exported = torch.export.export( wrapper, (export_images,), dynamic_shapes={ "images": { 0: torch.export.Dim("batch", min=1, max=4), - 2: 336 * height, - 3: 336 * width, + 2: 1008, + 3: 1008, } }, strict=False, diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..3b681d5 --- /dev/null +++ b/uv.lock @@ -0,0 +1,5825 @@ +version = 1 +revision = 3 +requires-python = ">=3.9, <3.13" +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] + +[[package]] +name = "absl-py" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2a/c93173ffa1b39c1d0395b7e842bbdc62e556ca9d8d3b5572926f3e4ca752/absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9", size = 116588, upload-time = "2025-07-03T09:31:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" }, +] + +[[package]] +name = "absl-py" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { 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 = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/ba4e4ca8d149f8dcc0d952ac0967089e1d759c7e5fcf0865a317eb680fbb/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e", size = 24549, upload-time = "2025-07-30T10:02:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/9b2386cc75ac0bd3210e12a44bfc7fd1632065ed8b80d573036eecb10442/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d", size = 25539, upload-time = "2025-07-30T10:02:00.929Z" }, + { url = "https://files.pythonhosted.org/packages/31/db/740de99a37aa727623730c90d92c22c9e12585b3c98c54b7960f7810289f/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584", size = 28467, upload-time = "2025-07-30T10:02:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/47c4509ea18d755f44e2b92b7178914f0c113946d11e16e626df8eaa2b0b/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690", size = 27355, upload-time = "2025-07-30T10:02:02.867Z" }, + { url = "https://files.pythonhosted.org/packages/ee/82/82745642d3c46e7cea25e1885b014b033f4693346ce46b7f47483cf5d448/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520", size = 29187, upload-time = "2025-07-30T10:02:03.674Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "async-lru" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/4d/71ec4d3939dc755264f680f6c2b4906423a304c3d18e96853f0a595dfe97/async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb", size = 10380, upload-time = "2025-03-16T17:25:36.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069, upload-time = "2025-03-16T17:25:35.422Z" }, +] + +[[package]] +name = "async-lru" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/c3/bbf34f15ea88dfb649ab2c40f9d75081784a50573a9ea431563cab64adb8/async_lru-2.1.0.tar.gz", hash = "sha256:9eeb2fecd3fe42cc8a787fc32ead53a3a7158cc43d039c3c55ab3e4e5b2a80ed", size = 12041, upload-time = "2026-01-17T22:52:18.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/e9/eb6a5db5ac505d5d45715388e92bced7a5bb556facc4d0865d192823f2d2/async_lru-2.1.0-py3-none-any.whl", hash = "sha256:fa12dcf99a42ac1280bc16c634bbaf06883809790f6304d85cdab3f666f33a7e", size = 6933, upload-time = "2026-01-17T22:52:17.389Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "black" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/69/f3ab49cdb938b3eecb048fa64f86bdadb1fac26e92c435d287181d543b0a/black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894", size = 631598, upload-time = "2024-02-12T20:21:26.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/75/25d478751e03a46663b3ae9ff961deb3c2ce1b24885f6fc010df5ba08b1d/black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29", size = 1575905, upload-time = "2024-02-12T20:32:30.688Z" }, + { url = "https://files.pythonhosted.org/packages/88/12/59b6ed8eefa43a0b131a97db5820141b94f6722c627cb10346c5408cf7f3/black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430", size = 1423215, upload-time = "2024-02-12T20:31:24.135Z" }, + { url = "https://files.pythonhosted.org/packages/46/b8/70a3cab340301d480f601d483452e6e68da61202abad881f1a91250c2d27/black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f", size = 1730127, upload-time = "2024-02-12T20:23:44.397Z" }, + { url = "https://files.pythonhosted.org/packages/64/5f/74be45f22ac104b4759084c792798f6b5695a5b4b2064e31222fea7fd3c7/black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a", size = 1355109, upload-time = "2024-02-12T20:25:09.905Z" }, + { url = "https://files.pythonhosted.org/packages/6c/de/938fd8c271ee903212f3599d7ece0d4a930af667c51135d4e6d1fac50455/black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd", size = 1556863, upload-time = "2024-02-12T20:33:27.299Z" }, + { url = "https://files.pythonhosted.org/packages/11/15/4646143c19bc896a0cbc0c6663abf5751b465edefbc49491179454e2e2fd/black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2", size = 1403471, upload-time = "2024-02-12T20:33:11.704Z" }, + { url = "https://files.pythonhosted.org/packages/1a/53/0b82f36f0a8beee5388f6bcc3a1591e094c87a275c64532ac6ce69fb491f/black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92", size = 1710660, upload-time = "2024-02-12T20:24:29.273Z" }, + { url = "https://files.pythonhosted.org/packages/72/be/58ae1a41e1d174a05d074a7a10524f1cdf37df13be3ac314098ab9435550/black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23", size = 1361881, upload-time = "2024-02-12T20:25:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/43/1e/67c87a1fb39592aa944f35cc26892946ebe0a10aa324b87f9380b8753862/black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b", size = 1585288, upload-time = "2024-02-12T20:37:13.8Z" }, + { url = "https://files.pythonhosted.org/packages/5e/62/6437212cf40e40b74dbc7e134700a21cb21a9ac7e46ade940b5d4826456f/black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9", size = 1417360, upload-time = "2024-02-12T20:34:56.41Z" }, + { url = "https://files.pythonhosted.org/packages/36/8f/de0d339ae683422a8e15d6f74b8022d4947009c347d8c2178c303c68cc4d/black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693", size = 1739406, upload-time = "2024-02-12T20:23:59.596Z" }, + { url = "https://files.pythonhosted.org/packages/3e/58/89e5f5a1c4c5b66dc74eabe6337623d53b4d1c27fbbbe16defee53397f60/black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982", size = 1373310, upload-time = "2024-02-12T20:25:27.243Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/eb61bca6e1bb69529bc1295f17ca7a0ebadf56c480de37b2acb485dc04e2/black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8", size = 1575445, upload-time = "2024-02-12T20:41:38.354Z" }, + { url = "https://files.pythonhosted.org/packages/1f/44/41f6766ad9007b98478547ba5cceada721383ae7a81793c1180cb7ad8b30/black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8", size = 1422403, upload-time = "2024-02-12T20:37:16.005Z" }, + { url = "https://files.pythonhosted.org/packages/d4/76/0c4bbb734126cdbbd7e82f6df4b58e67e17742250b15f1bac6901f1e8d9b/black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540", size = 1728526, upload-time = "2024-02-12T20:23:42.22Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/7441c2d961140a2273a4047429b4cd30b663d76c8c375d66f7fd57ae10f5/black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31", size = 1354913, upload-time = "2024-02-12T20:24:53.441Z" }, + { url = "https://files.pythonhosted.org/packages/47/15/b3770bc3328685a53bc9c041136240146c5cd866a1f020c2cf47f2ff9683/black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6", size = 200610, upload-time = "2024-02-12T20:21:17.657Z" }, +] + +[[package]] +name = "bleach" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "webencodings", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083, upload-time = "2024-10-29T18:30:40.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406, upload-time = "2024-10-29T18:30:38.186Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "webencodings", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2", marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370, upload-time = "2024-08-27T21:00:03.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366, upload-time = "2024-08-27T20:50:09.947Z" }, + { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226, upload-time = "2024-08-27T20:50:16.1Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460, upload-time = "2024-08-27T20:50:22.536Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623, upload-time = "2024-08-27T20:50:28.806Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761, upload-time = "2024-08-27T20:50:35.126Z" }, + { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015, upload-time = "2024-08-27T20:50:40.318Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672, upload-time = "2024-08-27T20:50:55.643Z" }, + { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688, upload-time = "2024-08-27T20:51:11.293Z" }, + { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145, upload-time = "2024-08-27T20:51:15.2Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019, upload-time = "2024-08-27T20:51:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356, upload-time = "2024-08-27T20:51:24.146Z" }, + { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915, upload-time = "2024-08-27T20:51:28.683Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443, upload-time = "2024-08-27T20:51:33.675Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548, upload-time = "2024-08-27T20:51:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118, upload-time = "2024-08-27T20:51:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162, upload-time = "2024-08-27T20:51:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396, upload-time = "2024-08-27T20:52:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297, upload-time = "2024-08-27T20:52:21.843Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808, upload-time = "2024-08-27T20:52:25.163Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181, upload-time = "2024-08-27T20:52:29.13Z" }, + { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838, upload-time = "2024-08-27T20:52:33.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549, upload-time = "2024-08-27T20:52:39.179Z" }, + { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177, upload-time = "2024-08-27T20:52:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735, upload-time = "2024-08-27T20:52:51.05Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679, upload-time = "2024-08-27T20:52:58.473Z" }, + { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549, upload-time = "2024-08-27T20:53:06.593Z" }, + { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068, upload-time = "2024-08-27T20:53:23.442Z" }, + { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833, upload-time = "2024-08-27T20:53:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681, upload-time = "2024-08-27T20:53:43.05Z" }, + { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283, upload-time = "2024-08-27T20:53:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518, upload-time = "2024-08-27T20:56:01.333Z" }, + { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350, upload-time = "2024-08-27T20:56:05.432Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167, upload-time = "2024-08-27T20:56:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279, upload-time = "2024-08-27T20:56:15.41Z" }, + { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519, upload-time = "2024-08-27T20:56:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922, upload-time = "2024-08-27T20:56:26.983Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017, upload-time = "2024-08-27T20:56:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773, upload-time = "2024-08-27T20:56:58.58Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353, upload-time = "2024-08-27T20:57:02.718Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817, upload-time = "2024-08-27T20:57:06.328Z" }, + { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886, upload-time = "2024-08-27T20:57:10.863Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008, upload-time = "2024-08-27T20:57:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690, upload-time = "2024-08-27T20:57:19.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894, upload-time = "2024-08-27T20:57:23.873Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099, upload-time = "2024-08-27T20:57:28.58Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838, upload-time = "2024-08-27T20:57:32.913Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "coverage" +version = "7.13.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" }, + { url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663, upload-time = "2026-02-03T13:59:41.951Z" }, + { url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424, upload-time = "2026-02-03T13:59:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228, upload-time = "2026-02-03T13:59:44.816Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103, upload-time = "2026-02-03T13:59:46.271Z" }, + { url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107, upload-time = "2026-02-03T13:59:48.53Z" }, + { url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143, upload-time = "2026-02-03T13:59:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148, upload-time = "2026-02-03T13:59:52.025Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414, upload-time = "2026-02-03T13:59:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023, upload-time = "2026-02-03T13:59:55.454Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751, upload-time = "2026-02-03T13:59:57.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686, upload-time = "2026-02-03T13:59:58.825Z" }, + { url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" }, + { url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cc/560ad6f12010344d0778e268df5ba9aa990aacccc310d478bf82bf3d302c/coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", size = 252111, upload-time = "2026-02-03T14:00:05.639Z" }, + { url = "https://files.pythonhosted.org/packages/f0/66/3193985fb2c58e91f94cfbe9e21a6fdf941e9301fe2be9e92c072e9c8f8c/coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", size = 254217, upload-time = "2026-02-03T14:00:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c5/78/f0f91556bf1faa416792e537c523c5ef9db9b1d32a50572c102b3d7c45b3/coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", size = 250318, upload-time = "2026-02-03T14:00:09.224Z" }, + { url = "https://files.pythonhosted.org/packages/6f/aa/fc654e45e837d137b2c1f3a2cc09b4aea1e8b015acd2f774fa0f3d2ddeba/coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", size = 251909, upload-time = "2026-02-03T14:00:10.712Z" }, + { url = "https://files.pythonhosted.org/packages/73/4d/ab53063992add8a9ca0463c9d92cce5994a29e17affd1c2daa091b922a93/coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", size = 249971, upload-time = "2026-02-03T14:00:12.402Z" }, + { url = "https://files.pythonhosted.org/packages/29/25/83694b81e46fcff9899694a1b6f57573429cdd82b57932f09a698f03eea5/coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", size = 249692, upload-time = "2026-02-03T14:00:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ef/d68fc304301f4cb4bf6aefa0045310520789ca38dabdfba9dbecd3f37919/coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", size = 250597, upload-time = "2026-02-03T14:00:15.461Z" }, + { url = "https://files.pythonhosted.org/packages/8d/85/240ad396f914df361d0f71e912ddcedb48130c71b88dc4193fe3c0306f00/coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb", size = 221773, upload-time = "2026-02-03T14:00:17.462Z" }, + { url = "https://files.pythonhosted.org/packages/2f/71/165b3a6d3d052704a9ab52d11ea64ef3426745de517dda44d872716213a7/coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", size = 222711, upload-time = "2026-02-03T14:00:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/51/d0/0ddc9c5934cdd52639c5df1f1eb0fdab51bb52348f3a8d1c7db9c600d93a/coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", size = 221377, upload-time = "2026-02-03T14:00:20.968Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" }, + { url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/62/71/a18a53d1808e09b2e9ebd6b47dad5e92daf4c38b0686b4c4d1b2f3e42b7f/coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23", size = 254051, upload-time = "2026-02-03T14:00:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/4a/0a/eb30f6455d04c5a3396d0696cad2df0269ae7444bb322f86ffe3376f7bf9/coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34", size = 255160, upload-time = "2026-02-03T14:00:29.024Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/a45baac86274ce3ed842dbb84f14560c673ad30535f397d89164ec56c5df/coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8", size = 251709, upload-time = "2026-02-03T14:00:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/df/dd0dc12f30da11349993f3e218901fdf82f45ee44773596050c8f5a1fb25/coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a", size = 253083, upload-time = "2026-02-03T14:00:32.14Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/fc764c8389a8ce95cb90eb97af4c32f392ab0ac23ec57cadeefb887188d3/coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4", size = 251227, upload-time = "2026-02-03T14:00:34.721Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/d025e9da8f06f24c34d2da9873957cfc5f7e0d67802c3e34d0caa8452130/coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7", size = 250794, upload-time = "2026-02-03T14:00:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/45/c7/76bf35d5d488ec8f68682eb8e7671acc50a6d2d1c1182de1d2b6d4ffad3b/coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0", size = 252671, upload-time = "2026-02-03T14:00:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/bf/10/1921f1a03a7c209e1cb374f81a6b9b68b03cdb3ecc3433c189bc90e2a3d5/coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1", size = 221986, upload-time = "2026-02-03T14:00:40.442Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7c/f5d93297f8e125a80c15545edc754d93e0ed8ba255b65e609b185296af01/coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d", size = 222793, upload-time = "2026-02-03T14:00:42.106Z" }, + { url = "https://files.pythonhosted.org/packages/43/59/c86b84170015b4555ebabca8649bdf9f4a1f737a73168088385ed0f947c4/coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f", size = 221410, upload-time = "2026-02-03T14:00:43.726Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "(python_full_version >= '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, +] +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/53/1d/f7f2bcffe788aebd4325a34d8a976b219a0751c06707aa89c9e70355ceae/cuda_bindings-12.9.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9866ceec83e39337d1a1d64837864c964ad902992478caa288a0bc1be95f21aa", size = 12152579, upload-time = "2025-10-21T14:52:16.731Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/02/4dbe7568a42e46582248942f54dc64ad094769532adbe21e525e4edf7bc4/cuda_pathfinder-1.3.3-py3-none-any.whl", hash = "sha256:9984b664e404f7c134954a771be8775dfd6180ea1e1aef4a5a37d4be05d9bbb1", size = 27154, upload-time = "2025-12-04T22:35:08.996Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/be/8bd693a0b9d53d48c8978fa5d889e06f3b5b03e45fd1ea1e78267b4887cb/debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64", size = 2099192, upload-time = "2026-01-29T23:03:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/77/1b/85326d07432086a06361d493d2743edd0c4fc2ef62162be7f8618441ac37/debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642", size = 3088568, upload-time = "2026-01-29T23:03:31.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/60/3e08462ee3eccd10998853eb35947c416e446bfe2bc37dbb886b9044586c/debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2", size = 5284399, upload-time = "2026-01-29T23:03:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/09d49106e770fe558ced5e80df2e3c2ebee10e576eda155dcc5670473663/debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893", size = 5316388, upload-time = "2026-01-29T23:03:35.095Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6b/668f21567e3250463beb6a401e7d598baa2a0907224000d7f68b9442c243/debugpy-1.8.20-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:bff8990f040dacb4c314864da95f7168c5a58a30a66e0eea0fb85e2586a92cd6", size = 2100484, upload-time = "2026-01-29T23:04:09.929Z" }, + { url = "https://files.pythonhosted.org/packages/cf/49/223143d1da586b891f35a45515f152742ad85bfc10d2e02e697f65c83b32/debugpy-1.8.20-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:70ad9ae09b98ac307b82c16c151d27ee9d68ae007a2e7843ba621b5ce65333b5", size = 3081272, upload-time = "2026-01-29T23:04:11.664Z" }, + { url = "https://files.pythonhosted.org/packages/b1/24/9f219c9290fe8bee4f63f9af8ebac440c802e6181d7f39a79abcb5fdff2f/debugpy-1.8.20-cp39-cp39-win32.whl", hash = "sha256:9eeed9f953f9a23850c85d440bf51e3c56ed5d25f8560eeb29add815bd32f7ee", size = 5285196, upload-time = "2026-01-29T23:04:13.105Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f3/4a12d7b1b09e3b79ba6e3edfa0c677b8b8bdf110bc4b3607e0f29fb4e8b3/debugpy-1.8.20-cp39-cp39-win_amd64.whl", hash = "sha256:760813b4fff517c75bfe7923033c107104e76acfef7bda011ffea8736e9a66f8", size = 5317163, upload-time = "2026-01-29T23:04:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "decord" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/79/936af42edf90a7bd4e41a6cac89c913d4b47fa48a26b042d5129a9242ee3/decord-0.6.0-py3-none-manylinux2010_x86_64.whl", hash = "sha256:51997f20be8958e23b7c4061ba45d0efcd86bffd5fe81c695d0befee0d442976", size = 13602299, upload-time = "2021-06-14T21:30:55.486Z" }, + { url = "https://files.pythonhosted.org/packages/6c/be/e15b5b866da452e62635a7b27513f31cb581fa2ea9cc9b768b535d62a955/decord-0.6.0-py3-none-win_amd64.whl", hash = "sha256:02665d7c4f1193a330205a791bc128f7e108eb6ae5b67144437a02f700943bad", size = 24733380, upload-time = "2021-06-14T21:30:57.766Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fairscale" +version = "0.4.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/08/b3334d7b543ac10dcb129cef4f84723ab696725512f18d69ab3a784b0bf5/fairscale-0.4.13.tar.gz", hash = "sha256:1b797825c427f5dba92253fd0d8daa574e8bd651a2423497775fab1b30cfb768", size = 266261, upload-time = "2022-12-11T18:09:16.892Z" } + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "fonttools" +version = "4.60.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/c4/db6a7b5eb0656534c3aa2596c2c5e18830d74f1b9aa5aa8a7dff63a0b11d/fonttools-4.60.2.tar.gz", hash = "sha256:d29552e6b155ebfc685b0aecf8d429cb76c14ab734c22ef5d3dea6fdf800c92c", size = 3562254, upload-time = "2025-12-09T13:38:11.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/de/9e10a99fb3070accb8884886a41a4ce54e49bf2fa4fc63f48a6cf2061713/fonttools-4.60.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e36fadcf7e8ca6e34d490eef86ed638d6fd9c55d2f514b05687622cfc4a7050", size = 2850403, upload-time = "2025-12-09T13:35:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/e4/40/d5b369d1073b134f600a94a287e13b5bdea2191ba6347d813fa3da00e94a/fonttools-4.60.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e500fc9c04bee749ceabfc20cb4903f6981c2139050d85720ea7ada61b75d5c", size = 2398629, upload-time = "2025-12-09T13:35:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b5/123819369aaf99d1e4dc49f1de1925d4edc7379114d15a56a7dd2e9d56e6/fonttools-4.60.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22efea5e784e1d1cd8d7b856c198e360a979383ebc6dea4604743b56da1cbc34", size = 4893471, upload-time = "2025-12-09T13:35:58.927Z" }, + { url = "https://files.pythonhosted.org/packages/24/29/f8f8acccb9716b899be4be45e9ce770d6aa76327573863e68448183091b0/fonttools-4.60.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:677aa92d84d335e4d301d8ba04afca6f575316bc647b6782cb0921943fcb6343", size = 4854686, upload-time = "2025-12-09T13:36:01.767Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0d/f3f51d7519f44f2dd5c9a60d7cd41185ebcee4348f073e515a3a93af15ff/fonttools-4.60.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:edd49d3defbf35476e78b61ff737ff5efea811acff68d44233a95a5a48252334", size = 4871233, upload-time = "2025-12-09T13:36:06.094Z" }, + { url = "https://files.pythonhosted.org/packages/cc/3f/4d4fd47d3bc40ab4d76718555185f8adffb5602ea572eac4bbf200c47d22/fonttools-4.60.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:126839492b69cecc5baf2bddcde60caab2ffafd867bbae2a88463fce6078ca3a", size = 4988936, upload-time = "2025-12-09T13:36:08.42Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/83bbdefa43f2c3ae206fd8c4b9a481f3c913eef871b1ce9a453069239e39/fonttools-4.60.2-cp310-cp310-win32.whl", hash = "sha256:ffcab6f5537136046ca902ed2491ab081ba271b07591b916289b7c27ff845f96", size = 2278044, upload-time = "2025-12-09T13:36:10.641Z" }, + { url = "https://files.pythonhosted.org/packages/d4/04/7d9a137e919d6c9ef26704b7f7b2580d9cfc5139597588227aacebc0e3b7/fonttools-4.60.2-cp310-cp310-win_amd64.whl", hash = "sha256:9c68b287c7ffcd29dd83b5f961004b2a54a862a88825d52ea219c6220309ba45", size = 2326522, upload-time = "2025-12-09T13:36:12.981Z" }, + { url = "https://files.pythonhosted.org/packages/e0/80/b7693d37c02417e162cc83cdd0b19a4f58be82c638b5d4ce4de2dae050c4/fonttools-4.60.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2aed0a7931401b3875265717a24c726f87ecfedbb7b3426c2ca4d2812e281ae", size = 2847809, upload-time = "2025-12-09T13:36:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9a/9c2c13bf8a6496ac21607d704e74e9cc68ebf23892cf924c9a8b5c7566b9/fonttools-4.60.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea6868e9d2b816c9076cfea77754686f3c19149873bdbc5acde437631c15df1", size = 2397302, upload-time = "2025-12-09T13:36:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/56/f6/ce38ff6b2d2d58f6fd981d32f3942365bfa30eadf2b47d93b2d48bf6097f/fonttools-4.60.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fa27f34950aa1fe0f0b1abe25eed04770a3b3b34ad94e5ace82cc341589678a", size = 5054418, upload-time = "2025-12-09T13:36:19.062Z" }, + { url = "https://files.pythonhosted.org/packages/88/06/5353bea128ff39e857c31de3dd605725b4add956badae0b31bc9a50d4c8e/fonttools-4.60.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13a53d479d187b09bfaa4a35ffcbc334fc494ff355f0a587386099cb66674f1e", size = 5031652, upload-time = "2025-12-09T13:36:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/71/05/ebca836437f6ebd57edd6428e7eff584e683ff0556ddb17d62e3b731f46c/fonttools-4.60.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fac5e921d3bd0ca3bb8517dced2784f0742bc8ca28579a68b139f04ea323a779", size = 5030321, upload-time = "2025-12-09T13:36:23.515Z" }, + { url = "https://files.pythonhosted.org/packages/57/f9/eb9d2a2ce30c99f840c1cc3940729a970923cf39d770caf88909d98d516b/fonttools-4.60.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:648f4f9186fd7f1f3cd57dbf00d67a583720d5011feca67a5e88b3a491952cfb", size = 5154255, upload-time = "2025-12-09T13:36:25.879Z" }, + { url = "https://files.pythonhosted.org/packages/08/a2/088b6ceba8272a9abb629d3c08f9c1e35e5ce42db0ccfe0c1f9f03e60d1d/fonttools-4.60.2-cp311-cp311-win32.whl", hash = "sha256:3274e15fad871bead5453d5ce02658f6d0c7bc7e7021e2a5b8b04e2f9e40da1a", size = 2276300, upload-time = "2025-12-09T13:36:27.772Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/8e4c3d908cc5dade7bb1316ce48589f6a24460c1056fd4b8db51f1fa309a/fonttools-4.60.2-cp311-cp311-win_amd64.whl", hash = "sha256:91d058d5a483a1525b367803abb69de0923fbd45e1f82ebd000f5c8aa65bc78e", size = 2327574, upload-time = "2025-12-09T13:36:30.89Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/530c9eddcd1c39219dc0aaede2b5a4c8ab80e0bb88d1b3ffc12944c4aac3/fonttools-4.60.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e0164b7609d2b5c5dd4e044b8085b7bd7ca7363ef8c269a4ab5b5d4885a426b2", size = 2847196, upload-time = "2025-12-09T13:36:33.262Z" }, + { url = "https://files.pythonhosted.org/packages/19/2f/4077a482836d5bbe3bc9dac1c004d02ee227cf04ed62b0a2dfc41d4f0dfd/fonttools-4.60.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1dd3d9574fc595c1e97faccae0f264dc88784ddf7fbf54c939528378bacc0033", size = 2395842, upload-time = "2025-12-09T13:36:35.47Z" }, + { url = "https://files.pythonhosted.org/packages/dd/05/aae5bb99c5398f8ed4a8b784f023fd9dd3568f0bd5d5b21e35b282550f11/fonttools-4.60.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98d0719f1b11c2817307d2da2e94296a3b2a3503f8d6252a101dca3ee663b917", size = 4949713, upload-time = "2025-12-09T13:36:37.874Z" }, + { url = "https://files.pythonhosted.org/packages/b4/37/49067349fc78ff0efbf09fadefe80ddf41473ca8f8a25400e3770da38328/fonttools-4.60.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d3ea26957dd07209f207b4fff64c702efe5496de153a54d3b91007ec28904dd", size = 4999907, upload-time = "2025-12-09T13:36:39.853Z" }, + { url = "https://files.pythonhosted.org/packages/16/31/d0f11c758bd0db36b664c92a0f9dfdcc2d7313749aa7d6629805c6946f21/fonttools-4.60.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ee301273b0850f3a515299f212898f37421f42ff9adfc341702582ca5073c13", size = 4939717, upload-time = "2025-12-09T13:36:43.075Z" }, + { url = "https://files.pythonhosted.org/packages/d9/bc/1cff0d69522e561bf1b99bee7c3911c08c25e919584827c3454a64651ce9/fonttools-4.60.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6eb4694cc3b9c03b7c01d65a9cf35b577f21aa6abdbeeb08d3114b842a58153", size = 5089205, upload-time = "2025-12-09T13:36:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e6/fb174f0069b7122e19828c551298bfd34fdf9480535d2a6ac2ed37afacd3/fonttools-4.60.2-cp312-cp312-win32.whl", hash = "sha256:57f07b616c69c244cc1a5a51072eeef07dddda5ebef9ca5c6e9cf6d59ae65b70", size = 2264674, upload-time = "2025-12-09T13:36:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/75/57/6552ffd6b582d3e6a9f01780c5275e6dfff1e70ca146101733aa1c12a129/fonttools-4.60.2-cp312-cp312-win_amd64.whl", hash = "sha256:310035802392f1fe5a7cf43d76f6ff4a24c919e4c72c0352e7b8176e2584b8a0", size = 2314701, upload-time = "2025-12-09T13:36:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/a6d9446cb258d3fe87e311c2d7bacf8e8da3e5809fbdc3a8306db4f6b14e/fonttools-4.60.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a3c75b8b42f7f93906bdba9eb1197bb76aecbe9a0a7cf6feec75f7605b5e8008", size = 2857184, upload-time = "2025-12-09T13:37:49.96Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f3/1b41d0b6a8b908aa07f652111155dd653ebbf0b3385e66562556c5206685/fonttools-4.60.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f86c8c37bc0ec0b9c141d5e90c717ff614e93c187f06d80f18c7057097f71bc", size = 2401877, upload-time = "2025-12-09T13:37:52.307Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/048fd781680c38b05c5463657d0d95d5f2391a51972176e175c01de29d42/fonttools-4.60.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe905403fe59683b0e9a45f234af2866834376b8821f34633b1c76fb731b6311", size = 4878073, upload-time = "2025-12-09T13:37:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/363364f052a893cebd3d449588b21244a9d873620fda03ad92702d2e1bc7/fonttools-4.60.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38ce703b60a906e421e12d9e3a7f064883f5e61bb23e8961f4be33cfe578500b", size = 4835385, upload-time = "2025-12-09T13:37:58.882Z" }, + { url = "https://files.pythonhosted.org/packages/1c/38/e392bb930b2436287e6021672345db26441bf1f85f1e98f8b9784334e41d/fonttools-4.60.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9e810c06f3e79185cecf120e58b343ea5a89b54dd695fd644446bcf8c026da5e", size = 4853084, upload-time = "2025-12-09T13:38:01.578Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/0d77faeaecf7a3276a8a6dc49e2274357e6b3ed6a1774e2fdb2a7f142db0/fonttools-4.60.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:38faec8cc1d12122599814d15a402183f5123fb7608dac956121e7c6742aebc5", size = 4971144, upload-time = "2025-12-09T13:38:03.748Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/6d3ac3afbcd598631bce24c3ecb919e7d0644a82fea8ddc4454312fc0be6/fonttools-4.60.2-cp39-cp39-win32.whl", hash = "sha256:80a45cf7bf659acb7b36578f300231873daba67bd3ca8cce181c73f861f14a37", size = 1499411, upload-time = "2025-12-09T13:38:05.586Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/9dedf6420e23f9fa630bb97941839dddd2e1e57d1b2b85a902378dbe0bd2/fonttools-4.60.2-cp39-cp39-win_amd64.whl", hash = "sha256:c355d5972071938e1b1e0f5a1df001f68ecf1a62f34a3407dc8e0beccf052501", size = 1547943, upload-time = "2025-12-09T13:38:07.604Z" }, + { url = "https://files.pythonhosted.org/packages/79/6c/10280af05b44fafd1dff69422805061fa1af29270bc52dce031ac69540bf/fonttools-4.60.2-py3-none-any.whl", hash = "sha256:73cf92eeda67cf6ff10c8af56fc8f4f07c1647d989a979be9e388a49be26552a", size = 1144610, upload-time = "2025-12-09T13:38:09.5Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, + { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" }, + { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" }, + { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, + { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, + { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, + { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +] + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, +] + +[[package]] +name = "ftfy" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/16/79c6e17bd3465f6498282dd23813846c68cd0989fe60bfef68bb1918d041/ftfy-6.1.1.tar.gz", hash = "sha256:bfc2019f84fcd851419152320a6375604a0f1459c281b5b199b2cd0d2e727f8f", size = 63156, upload-time = "2022-02-09T19:44:17.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/1e/bf736f9576a8979752b826b75cbd83663ff86634ea3055a766e2d8ad3ee5/ftfy-6.1.1-py3-none-any.whl", hash = "sha256:0ffd33fce16b54cccaec78d6ec73d95ad370e5df5a25255c8966a6147bd667ca", size = 53098, upload-time = "2022-02-09T19:44:15.655Z" }, +] + +[[package]] +name = "fvcore" +version = "0.1.5.post20221221" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "iopath" }, + { name = "numpy" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml" }, + { name = "tabulate" }, + { name = "termcolor", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "termcolor", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tqdm" }, + { name = "yacs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/93/d056a9c4efc6c79ba7b5159cc66bb436db93d2cc46dca18ed65c59cc8e4e/fvcore-0.1.5.post20221221.tar.gz", hash = "sha256:f2fb0bb90572ae651c11c78e20493ed19b2240550a7e4bbb2d6de87bdd037860", size = 50217, upload-time = "2022-12-21T08:10:53.563Z" } + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.31" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/11/2b0f60686dbda49028cec8c66bd18a5e82c96d92eef4bc34961e35bb3762/GitPython-3.1.31.tar.gz", hash = "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573", size = 195822, upload-time = "2023-02-16T16:33:16.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/8a/d1e02cc111d65b0346f70abb83c51f8593e7134bf694a4a56d1a470caaf7/GitPython-3.1.31-py3-none-any.whl", hash = "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d", size = 184332, upload-time = "2023-02-16T16:33:14.13Z" }, +] + +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" }, + { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" }, + { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" }, + { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, + { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d5/301e71c7d22a5c7aabf1953dd1106987bd47f883377d528355f898a850f2/grpcio-1.76.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:8ebe63ee5f8fa4296b1b8cfc743f870d10e902ca18afc65c68cf46fd39bb0783", size = 5840371, upload-time = "2025-10-21T16:22:42.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/55/e3181adccff8808301dd9214b5e03c6db5a404b5ae8a6ec5768a5a65ed63/grpcio-1.76.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:3bf0f392c0b806905ed174dcd8bdd5e418a40d5567a05615a030a5aeddea692d", size = 11840384, upload-time = "2025-10-21T16:22:45.508Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/db1dfe943bce7180f5b6d9be564366ca1024a005e914a1f10212c24a840b/grpcio-1.76.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b7604868b38c1bfd5cf72d768aedd7db41d78cb6a4a18585e33fb0f9f2363fd", size = 6408765, upload-time = "2025-10-21T16:22:48.761Z" }, + { url = "https://files.pythonhosted.org/packages/1e/79/a8452764aa4b5ca30a970e514ec2fc5cf75451571793f6b276b6807f67dc/grpcio-1.76.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e6d1db20594d9daba22f90da738b1a0441a7427552cc6e2e3d1297aeddc00378", size = 7076220, upload-time = "2025-10-21T16:22:51.546Z" }, + { url = "https://files.pythonhosted.org/packages/e0/61/4cca38c4e7bb3ac5a1e0be6cf700a4dd85c61cbd8a9c5e076c224967084e/grpcio-1.76.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d099566accf23d21037f18a2a63d323075bebace807742e4b0ac210971d4dd70", size = 6610195, upload-time = "2025-10-21T16:22:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/54/3d/3f8bfae264c22c95fa702c35aa2a8105b754b4ace049c66a8b2230c97671/grpcio-1.76.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ebea5cc3aa8ea72e04df9913492f9a96d9348db876f9dda3ad729cfedf7ac416", size = 7193343, upload-time = "2025-10-21T16:22:57.434Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cd/89f9254782b6cd94aa7c93fde370862877113b7189fb49900eaf9a706c82/grpcio-1.76.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0c37db8606c258e2ee0c56b78c62fc9dee0e901b5dbdcf816c2dd4ad652b8b0c", size = 8161922, upload-time = "2025-10-21T16:23:00.135Z" }, + { url = "https://files.pythonhosted.org/packages/af/e0/99eb899d7cb9c676afea70ab6d02a72a9e6ce24d0300f625773fafe6d547/grpcio-1.76.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ebebf83299b0cb1721a8859ea98f3a77811e35dce7609c5c963b9ad90728f886", size = 7617951, upload-time = "2025-10-21T16:23:03.68Z" }, + { url = "https://files.pythonhosted.org/packages/79/26/dca1b2bfaa9981cc28fa995730c80eedb0b86c912c30d1b676f08232e6ab/grpcio-1.76.0-cp39-cp39-win32.whl", hash = "sha256:0aaa82d0813fd4c8e589fac9b65d7dd88702555f702fb10417f96e2a2a6d4c0f", size = 3999306, upload-time = "2025-10-21T16:23:06.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fb90564a981eedd3cd87dc6bfd7c249e8a515cfad1ed8e9af73be223cd3b/grpcio-1.76.0-cp39-cp39-win_amd64.whl", hash = "sha256:acab0277c40eff7143c2323190ea57b9ee5fd353d8190ee9652369fae735668a", size = 4708771, upload-time = "2025-10-21T16:23:08.902Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +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 = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "fsspec", version = "2026.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "shellingham" }, + { name = "tqdm" }, + { name = "typer-slim" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/0e/e73927175162b8a4702b9f59268860f441fbe037c3960b1b6791eeb1deb7/huggingface_hub-1.4.0.tar.gz", hash = "sha256:dd8ca29409be10f544b624265f7ffe13a1a5c3f049f493b5dc9816ef3c6bd57b", size = 641608, upload-time = "2026-02-04T13:48:55.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/74/f0fb3a54fbca7c0aeff85f41d93b90ca3f6a36d918459401a3890763c54b/huggingface_hub-1.4.0-py3-none-any.whl", hash = "sha256:49d380ffddb31d9d4b6acc0792691f8fa077e1ed51980ed42c7abca62ec1b3b6", size = 553202, upload-time = "2026-02-04T13:48:53.545Z" }, +] + +[[package]] +name = "hydra-core" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "omegaconf" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547, upload-time = "2023-02-23T18:33:40.801Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "iopath" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "portalocker" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/73/b3d451dfc523756cf177d3ebb0af76dc7751b341c60e2a21871be400ae29/iopath-0.1.10.tar.gz", hash = "sha256:3311c16a4d9137223e20f141655759933e1eda24f8bff166af834af3c645ef01", size = 42226, upload-time = "2022-07-09T19:00:50.866Z" } + +[[package]] +name = "ipycanvas" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipywidgets" }, + { name = "numpy" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/56/484c8979bbcaa3e3f2da4eac6a1eb41e998e353e4c6ef89e9612889813c8/ipycanvas-0.14.3.tar.gz", hash = "sha256:c6a53a22eebf4d611b168b8f4434145883f27a7575509bd99a4bfc48c5385a39", size = 4150499, upload-time = "2025-12-11T09:12:59.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/37/c6880bd16093793dcb4c005011cf968f45fd815b7b5094fa8374524add26/ipycanvas-0.14.3-py2.py3-none-any.whl", hash = "sha256:8a2f48e1e079355d3e7d5683e5c6e7684a87c15c3750c8d8cd2289c95383ee3e", size = 142962, upload-time = "2025-12-11T09:12:50.5Z" }, +] + +[[package]] +name = "ipykernel" +version = "6.31.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "appnope", marker = "python_full_version < '3.10' and sys_platform == 'darwin'" }, + { name = "comm", marker = "python_full_version < '3.10'" }, + { name = "debugpy", marker = "python_full_version < '3.10'" }, + { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-client", version = "8.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, + { name = "nest-asyncio", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "psutil", marker = "python_full_version < '3.10'" }, + { name = "pyzmq", marker = "python_full_version < '3.10'" }, + { name = "tornado", marker = "python_full_version < '3.10'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/1d/d5ba6edbfe6fae4c3105bca3a9c889563cc752c7f2de45e333164c7f4846/ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6", size = 167493, upload-time = "2025-10-20T11:42:39.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af", size = 117003, upload-time = "2025-10-20T11:42:37.502Z" }, +] + +[[package]] +name = "ipykernel" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "appnope", marker = "python_full_version >= '3.10' and sys_platform == 'darwin'" }, + { name = "comm", marker = "python_full_version >= '3.10'" }, + { name = "debugpy", marker = "python_full_version >= '3.10'" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jupyter-client", version = "8.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.10'" }, + { name = "nest-asyncio", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "psutil", marker = "python_full_version >= '3.10'" }, + { name = "pyzmq", marker = "python_full_version >= '3.10'" }, + { name = "tornado", marker = "python_full_version >= '3.10'" }, + { name = "traitlets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c", size = 117968, upload-time = "2025-10-27T09:46:37.805Z" }, +] + +[[package]] +name = "ipympl" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipywidgets" }, + { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/9c/f79e29f6262e821a15757662aa11cbb1db0a51ef836a32a46ddcb25e6832/ipympl-0.10.0.tar.gz", hash = "sha256:eda69602a010af2a42e8ebd069b0ee0dbe8df7fc69d7c1e8b99fece0a2fe613f", size = 3595672, upload-time = "2026-01-21T20:19:47.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/88c0ef22878c86035f058df0ac6c171319ffd0aa52a406455ed3a3847566/ipympl-0.10.0-py3-none-any.whl", hash = "sha256:a09c4f0ff86490cc62aed45e53b912fb706e3ec3506c4a51ce4a670d6667f5ce", size = 519020, upload-time = "2026-01-21T20:19:46.325Z" }, +] + +[[package]] +name = "ipython" +version = "8.18.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.10'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "jedi", marker = "python_full_version < '3.10'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, + { name = "pexpect", marker = "python_full_version < '3.10' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "stack-data", marker = "python_full_version < '3.10'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330, upload-time = "2023-11-27T09:58:34.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161, upload-time = "2023-11-27T09:58:30.538Z" }, +] + +[[package]] +name = "ipython" +version = "8.38.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version == '3.10.*'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "jedi", marker = "python_full_version == '3.10.*'" }, + { name = "matplotlib-inline", marker = "python_full_version == '3.10.*'" }, + { name = "pexpect", marker = "python_full_version == '3.10.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "stack-data", marker = "python_full_version == '3.10.*'" }, + { name = "traitlets", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" }, +] + +[[package]] +name = "ipython" +version = "9.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "ipywidgets" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "json5" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size = 52441, upload-time = "2026-01-01T19:42:14.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "attrs", marker = "python_full_version < '3.10'" }, + { name = "jsonschema-specifications", marker = "python_full_version < '3.10'" }, + { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "isoduration", marker = "python_full_version < '3.10'" }, + { name = "jsonpointer", marker = "python_full_version < '3.10'" }, + { name = "rfc3339-validator", marker = "python_full_version < '3.10'" }, + { name = "rfc3986-validator", marker = "python_full_version < '3.10'" }, + { name = "rfc3987-syntax", marker = "python_full_version < '3.10'" }, + { name = "uri-template", marker = "python_full_version < '3.10'" }, + { name = "webcolors", version = "24.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" }, + { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn", marker = "python_full_version >= '3.10'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "isoduration", marker = "python_full_version >= '3.10'" }, + { name = "jsonpointer", marker = "python_full_version >= '3.10'" }, + { name = "rfc3339-validator", marker = "python_full_version >= '3.10'" }, + { name = "rfc3986-validator", marker = "python_full_version >= '3.10'" }, + { name = "rfc3987-syntax", marker = "python_full_version >= '3.10'" }, + { name = "uri-template", marker = "python_full_version >= '3.10'" }, + { name = "webcolors", version = "25.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipykernel", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ipywidgets" }, + { name = "jupyter-console" }, + { name = "jupyterlab" }, + { name = "nbconvert" }, + { name = "notebook" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959, upload-time = "2024-08-30T07:15:48.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, + { name = "pyzmq", marker = "python_full_version < '3.10'" }, + { name = "tornado", marker = "python_full_version < '3.10'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, + { name = "pyzmq", marker = "python_full_version >= '3.10'" }, + { name = "tornado", marker = "python_full_version >= '3.10'" }, + { name = "traitlets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-console" +version = "6.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipykernel", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jupyter-client", version = "8.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-client", version = "8.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "pyzmq" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/2d/e2fd31e2fc41c14e2bcb6c976ab732597e907523f6b2420305f9fc7fdbdb/jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539", size = 34363, upload-time = "2023-03-06T14:13:31.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510, upload-time = "2023-03-06T14:13:28.229Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.8.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pywin32", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "traitlets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyter-events" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, extra = ["format-nongpl"], marker = "python_full_version < '3.10'" }, + { name = "jsonschema", version = "4.26.0", source = { registry = "https://pypi.org/simple" }, extra = ["format-nongpl"], marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" }, +] + +[[package]] +name = "jupyter-server" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client", version = "8.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-client", version = "8.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "overrides", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "(python_full_version < '3.10' and os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "(python_full_version < '3.10' and os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, +] + +[[package]] +name = "jupyterlab" +version = "4.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru", version = "2.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "async-lru", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "httpx" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ipykernel", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jinja2" }, + { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "setuptools" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/76/393eae3349f9a39bf21f8f5406e5244d36e2bfc932049b6070c271f92764/jupyterlab-4.5.3.tar.gz", hash = "sha256:4a159f71067cb38e4a82e86a42de8e7e926f384d7f2291964f282282096d27e8", size = 23939231, upload-time = "2026-01-23T15:04:25.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/9a/0bf9a7a45f0006d7ff4fdc4fc313de4255acab02bf4db1887c65f0472c01/jupyterlab-4.5.3-py3-none-any.whl", hash = "sha256:63c9f3a48de72ba00df766ad6eed416394f5bb883829f11eeff0872302520ba7", size = 12391761, upload-time = "2026-01-23T15:04:21.214Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jsonschema", version = "4.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440, upload-time = "2024-09-04T09:03:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758, upload-time = "2024-09-04T09:03:46.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311, upload-time = "2024-09-04T09:03:47.973Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109, upload-time = "2024-09-04T09:03:49.281Z" }, + { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814, upload-time = "2024-09-04T09:03:51.444Z" }, + { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881, upload-time = "2024-09-04T09:03:53.357Z" }, + { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972, upload-time = "2024-09-04T09:03:55.082Z" }, + { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787, upload-time = "2024-09-04T09:03:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212, upload-time = "2024-09-04T09:03:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399, upload-time = "2024-09-04T09:04:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688, upload-time = "2024-09-04T09:04:02.216Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493, upload-time = "2024-09-04T09:04:04.571Z" }, + { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191, upload-time = "2024-09-04T09:04:05.969Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644, upload-time = "2024-09-04T09:04:07.408Z" }, + { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877, upload-time = "2024-09-04T09:04:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347, upload-time = "2024-09-04T09:04:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442, upload-time = "2024-09-04T09:04:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762, upload-time = "2024-09-04T09:04:12.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319, upload-time = "2024-09-04T09:04:13.635Z" }, + { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260, upload-time = "2024-09-04T09:04:14.878Z" }, + { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589, upload-time = "2024-09-04T09:04:16.514Z" }, + { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080, upload-time = "2024-09-04T09:04:18.322Z" }, + { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049, upload-time = "2024-09-04T09:04:20.266Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376, upload-time = "2024-09-04T09:04:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231, upload-time = "2024-09-04T09:04:24.526Z" }, + { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634, upload-time = "2024-09-04T09:04:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024, upload-time = "2024-09-04T09:04:28.523Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484, upload-time = "2024-09-04T09:04:30.547Z" }, + { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078, upload-time = "2024-09-04T09:04:33.218Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645, upload-time = "2024-09-04T09:04:34.371Z" }, + { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022, upload-time = "2024-09-04T09:04:35.786Z" }, + { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536, upload-time = "2024-09-04T09:04:37.525Z" }, + { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808, upload-time = "2024-09-04T09:04:38.637Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531, upload-time = "2024-09-04T09:04:39.694Z" }, + { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894, upload-time = "2024-09-04T09:04:41.6Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296, upload-time = "2024-09-04T09:04:42.886Z" }, + { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450, upload-time = "2024-09-04T09:04:46.284Z" }, + { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168, upload-time = "2024-09-04T09:04:47.91Z" }, + { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308, upload-time = "2024-09-04T09:04:49.465Z" }, + { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186, upload-time = "2024-09-04T09:04:50.949Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877, upload-time = "2024-09-04T09:04:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204, upload-time = "2024-09-04T09:04:54.385Z" }, + { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461, upload-time = "2024-09-04T09:04:56.307Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358, upload-time = "2024-09-04T09:04:57.922Z" }, + { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119, upload-time = "2024-09-04T09:04:59.332Z" }, + { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367, upload-time = "2024-09-04T09:05:00.804Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884, upload-time = "2024-09-04T09:05:01.924Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528, upload-time = "2024-09-04T09:05:02.983Z" }, + { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449, upload-time = "2024-09-04T09:05:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757, upload-time = "2024-09-04T09:05:56.906Z" }, + { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312, upload-time = "2024-09-04T09:05:58.384Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966, upload-time = "2024-09-04T09:05:59.855Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044, upload-time = "2024-09-04T09:06:02.16Z" }, + { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879, upload-time = "2024-09-04T09:06:03.908Z" }, + { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751, upload-time = "2024-09-04T09:06:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990, upload-time = "2024-09-04T09:06:08.126Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122, upload-time = "2024-09-04T09:06:10.345Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126, upload-time = "2024-09-04T09:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313, upload-time = "2024-09-04T09:06:14.562Z" }, + { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784, upload-time = "2024-09-04T09:06:16.767Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988, upload-time = "2024-09-04T09:06:18.705Z" }, + { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980, upload-time = "2024-09-04T09:06:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847, upload-time = "2024-09-04T09:06:21.407Z" }, + { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494, upload-time = "2024-09-04T09:06:22.648Z" }, + { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491, upload-time = "2024-09-04T09:06:24.188Z" }, + { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648, upload-time = "2024-09-04T09:06:25.559Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257, upload-time = "2024-09-04T09:06:27.038Z" }, + { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906, upload-time = "2024-09-04T09:06:28.48Z" }, + { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951, upload-time = "2024-09-04T09:06:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715, upload-time = "2024-09-04T09:06:31.489Z" }, + { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666, upload-time = "2024-09-04T09:06:43.756Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088, upload-time = "2024-09-04T09:06:45.406Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321, upload-time = "2024-09-04T09:06:47.557Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776, upload-time = "2024-09-04T09:06:49.235Z" }, + { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984, upload-time = "2024-09-04T09:06:51.336Z" }, + { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811, upload-time = "2024-09-04T09:06:53.078Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, + { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, + { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, + { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, + { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, + { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, + { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, + { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, + { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, +] + +[[package]] +name = "libcst" +version = "1.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/cd/337df968b38d94c5aabd3e1b10630f047a2b345f6e1d4456bd9fe7417537/libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b", size = 891354, upload-time = "2025-11-03T22:33:30.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/52/97d5454dee9d014821fe0c88f3dc0e83131b97dd074a4d49537056a75475/libcst-1.8.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a20c5182af04332cc94d8520792befda06d73daf2865e6dddc5161c72ea92cb9", size = 2211698, upload-time = "2025-11-03T22:31:50.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a4/d1205985d378164687af3247a9c8f8bdb96278b0686ac98ab951bc6d336a/libcst-1.8.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36473e47cb199b7e6531d653ee6ffed057de1d179301e6c67f651f3af0b499d6", size = 2093104, upload-time = "2025-11-03T22:31:52.189Z" }, + { url = "https://files.pythonhosted.org/packages/9e/de/1338da681b7625b51e584922576d54f1b8db8fc7ff4dc79121afc5d4d2cd/libcst-1.8.6-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:06fc56335a45d61b7c1b856bfab4587b84cfe31e9d6368f60bb3c9129d900f58", size = 2237419, upload-time = "2025-11-03T22:31:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/50/06/ee66f2d83b870534756e593d464d8b33b0914c224dff3a407e0f74dc04e0/libcst-1.8.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6b23d14a7fc0addd9795795763af26b185deb7c456b1e7cc4d5228e69dab5ce8", size = 2300820, upload-time = "2025-11-03T22:31:55.995Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ca/959088729de8e0eac8dd516e4fb8623d8d92bad539060fa85c9e94d418a5/libcst-1.8.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:16cfe0cfca5fd840e1fb2c30afb628b023d3085b30c3484a79b61eae9d6fe7ba", size = 2301201, upload-time = "2025-11-03T22:31:57.347Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4c/2a21a8c452436097dfe1da277f738c3517f3f728713f16d84b9a3d67ca8d/libcst-1.8.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:455f49a93aea4070132c30ebb6c07c2dea0ba6c1fde5ffde59fc45dbb9cfbe4b", size = 2408213, upload-time = "2025-11-03T22:31:59.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/26/8f7b671fad38a515bb20b038718fd2221ab658299119ac9bcec56c2ced27/libcst-1.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:72cca15800ffc00ba25788e4626189fe0bc5fe2a0c1cb4294bce2e4df21cc073", size = 2119189, upload-time = "2025-11-03T22:32:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/5b/bf/ffb23a48e27001165cc5c81c5d9b3d6583b21b7f5449109e03a0020b060c/libcst-1.8.6-cp310-cp310-win_arm64.whl", hash = "sha256:6cad63e3a26556b020b634d25a8703b605c0e0b491426b3e6b9e12ed20f09100", size = 2001736, upload-time = "2025-11-03T22:32:02.986Z" }, + { url = "https://files.pythonhosted.org/packages/dc/15/95c2ecadc0fb4af8a7057ac2012a4c0ad5921b9ef1ace6c20006b56d3b5f/libcst-1.8.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3649a813660fbffd7bc24d3f810b1f75ac98bd40d9d6f56d1f0ee38579021073", size = 2211289, upload-time = "2025-11-03T22:32:04.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/c3/7e1107acd5ed15cf60cc07c7bb64498a33042dc4821874aea3ec4942f3cd/libcst-1.8.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cbe17067055829607c5ba4afa46bfa4d0dd554c0b5a583546e690b7367a29b6", size = 2092927, upload-time = "2025-11-03T22:32:06.209Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ff/0d2be87f67e2841a4a37d35505e74b65991d30693295c46fc0380ace0454/libcst-1.8.6-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:59a7e388c57d21d63722018978a8ddba7b176e3a99bd34b9b84a576ed53f2978", size = 2237002, upload-time = "2025-11-03T22:32:07.559Z" }, + { url = "https://files.pythonhosted.org/packages/69/99/8c4a1b35c7894ccd7d33eae01ac8967122f43da41325223181ca7e4738fe/libcst-1.8.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b6c1248cc62952a3a005792b10cdef2a4e130847be9c74f33a7d617486f7e532", size = 2301048, upload-time = "2025-11-03T22:32:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8b/d1aa811eacf936cccfb386ae0585aa530ea1221ccf528d67144e041f5915/libcst-1.8.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6421a930b028c5ef4a943b32a5a78b7f1bf15138214525a2088f11acbb7d3d64", size = 2300675, upload-time = "2025-11-03T22:32:10.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6b/7b65cd41f25a10c1fef2389ddc5c2b2cc23dc4d648083fa3e1aa7e0eeac2/libcst-1.8.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6d8b67874f2188399a71a71731e1ba2d1a2c3173b7565d1cc7ffb32e8fbaba5b", size = 2407934, upload-time = "2025-11-03T22:32:11.856Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/401cfff374bb3b785adfad78f05225225767ee190997176b2a9da9ed9460/libcst-1.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:b0d8c364c44ae343937f474b2e492c1040df96d94530377c2f9263fb77096e4f", size = 2119247, upload-time = "2025-11-03T22:32:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/085f59eaa044b6ff6bc42148a5449df2b7f0ba567307de7782fe85c39ee2/libcst-1.8.6-cp311-cp311-win_arm64.whl", hash = "sha256:5dcaaebc835dfe5755bc85f9b186fb7e2895dda78e805e577fef1011d51d5a5c", size = 2001774, upload-time = "2025-11-03T22:32:14.647Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3c/93365c17da3d42b055a8edb0e1e99f1c60c776471db6c9b7f1ddf6a44b28/libcst-1.8.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c13d5bd3d8414a129e9dccaf0e5785108a4441e9b266e1e5e9d1f82d1b943c9", size = 2206166, upload-time = "2025-11-03T22:32:16.012Z" }, + { url = "https://files.pythonhosted.org/packages/1d/cb/7530940e6ac50c6dd6022349721074e19309eb6aa296e942ede2213c1a19/libcst-1.8.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1472eeafd67cdb22544e59cf3bfc25d23dc94058a68cf41f6654ff4fcb92e09", size = 2083726, upload-time = "2025-11-03T22:32:17.312Z" }, + { url = "https://files.pythonhosted.org/packages/1b/cf/7e5eaa8c8f2c54913160671575351d129170db757bb5e4b7faffed022271/libcst-1.8.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:089c58e75cb142ec33738a1a4ea7760a28b40c078ab2fd26b270dac7d2633a4d", size = 2235755, upload-time = "2025-11-03T22:32:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/570ec2b0e9a3de0af9922e3bb1b69a5429beefbc753a7ea770a27ad308bd/libcst-1.8.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c9d7aeafb1b07d25a964b148c0dda9451efb47bbbf67756e16eeae65004b0eb5", size = 2301473, upload-time = "2025-11-03T22:32:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/11/4c/163457d1717cd12181c421a4cca493454bcabd143fc7e53313bc6a4ad82a/libcst-1.8.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:207481197afd328aa91d02670c15b48d0256e676ce1ad4bafb6dc2b593cc58f1", size = 2298899, upload-time = "2025-11-03T22:32:21.765Z" }, + { url = "https://files.pythonhosted.org/packages/35/1d/317ddef3669883619ef3d3395ea583305f353ef4ad87d7a5ac1c39be38e3/libcst-1.8.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:375965f34cc6f09f5f809244d3ff9bd4f6cb6699f571121cebce53622e7e0b86", size = 2408239, upload-time = "2025-11-03T22:32:23.275Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a1/f47d8cccf74e212dd6044b9d6dbc223636508da99acff1d54786653196bc/libcst-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:da95b38693b989eaa8d32e452e8261cfa77fe5babfef1d8d2ac25af8c4aa7e6d", size = 2119660, upload-time = "2025-11-03T22:32:24.822Z" }, + { url = "https://files.pythonhosted.org/packages/19/d0/dd313bf6a7942cdf951828f07ecc1a7695263f385065edc75ef3016a3cb5/libcst-1.8.6-cp312-cp312-win_arm64.whl", hash = "sha256:bff00e1c766658adbd09a175267f8b2f7616e5ee70ce45db3d7c4ce6d9f6bec7", size = 1999824, upload-time = "2025-11-03T22:32:26.131Z" }, + { url = "https://files.pythonhosted.org/packages/0c/09/69a0cd1eeb358f03c3ccd79ca22778afc1c1c723158270ad84ce86266eed/libcst-1.8.6-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cb2679ef532f9fa5be5c5a283b6357cb6e9888a8dd889c4bb2b01845a29d8c0b", size = 2211812, upload-time = "2025-11-03T22:33:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/ff/38/b965fa7bc4409520404261ce6bdf019e56bed1674b9a68ddfc9e25bc904c/libcst-1.8.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:203ec2a83f259baf686b9526268cd23d048d38be5589594ef143aee50a4faf7e", size = 2093137, upload-time = "2025-11-03T22:33:19.457Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7c/083084b91db049343c49a27279c226f4eb27d28bef4942965386418e643e/libcst-1.8.6-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6366ab2107425bf934b0c83311177f2a371bfc757ee8c6ad4a602d7cbcc2f363", size = 2237609, upload-time = "2025-11-03T22:33:21.083Z" }, + { url = "https://files.pythonhosted.org/packages/26/c5/fcf60600a809b9e4cf75e82484a7a9a4bdc80ba3c9939a6a18af3379c6c7/libcst-1.8.6-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:6aa11df6c58812f731172b593fcb485d7ba09ccc3b52fea6c7f26a43377dc748", size = 2301394, upload-time = "2025-11-03T22:33:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/9f/73/d72942eb3f520bc9444e61a48236694dee3cdc13f6b59179e5288d725b93/libcst-1.8.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:351ab879c2fd20d9cb2844ed1ea3e617ed72854d3d1e2b0880ede9c3eea43ba8", size = 2301816, upload-time = "2025-11-03T22:33:24.295Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5732b20569a434ee3ff96f1b263e6e3f3df70d8dba5cf7c8f7d4b1d6aa41/libcst-1.8.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98fa1ca321c81fb1f02e5c43f956ca543968cc1a30b264fd8e0a2e1b0b0bf106", size = 2408392, upload-time = "2025-11-03T22:33:25.873Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ad/ecb1275796504a34a9d6d5d4f73bd81cb12930064e98871ad4b4042b82e1/libcst-1.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:25fc7a1303cad7639ad45ec38c06789b4540b7258e9a108924aaa2c132af4aca", size = 2119206, upload-time = "2025-11-03T22:33:27.642Z" }, + { url = "https://files.pythonhosted.org/packages/94/32/b6521d32a7cde089380efa948e05a7cff95c7ece8f7c36380dd6b4bf2263/libcst-1.8.6-cp39-cp39-win_arm64.whl", hash = "sha256:4d7bbdd35f3abdfb5ac5d1a674923572dab892b126a58da81ff2726102d6ec2e", size = 2001882, upload-time = "2025-11-03T22:33:29.06Z" }, +] + +[[package]] +name = "lightning-utilities" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "setuptools" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/39/6fc58ca81492db047149b4b8fd385aa1bfb8c28cd7cacb0c7eb0c44d842f/lightning_utilities-0.15.2.tar.gz", hash = "sha256:cdf12f530214a63dacefd713f180d1ecf5d165338101617b4742e8f22c032e24", size = 31090, upload-time = "2025-08-06T13:57:39.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/73/3d757cb3fc16f0f9794dd289bcd0c4a031d9cf54d8137d6b984b2d02edf3/lightning_utilities-0.15.2-py3-none-any.whl", hash = "sha256:ad3ab1703775044bbf880dbf7ddaaac899396c96315f3aa1779cec9d618a9841", size = 29431, upload-time = "2025-08-06T13:57:38.046Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.43.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/3d/f513755f285db51ab363a53e898b85562e950f79a2e6767a364530c2f645/llvmlite-0.43.0.tar.gz", hash = "sha256:ae2b5b5c3ef67354824fb75517c8db5fbe93bc02cd9671f3c62271626bc041d5", size = 157069, upload-time = "2024-06-13T18:09:32.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/ff/6ca7e98998b573b4bd6566f15c35e5c8bea829663a6df0c7aa55ab559da9/llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761", size = 31064408, upload-time = "2024-06-13T18:08:13.462Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5c/a27f9257f86f0cda3f764ff21d9f4217b9f6a0d45e7a39ecfa7905f524ce/llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc", size = 28793153, upload-time = "2024-06-13T18:08:17.336Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/4410f670ad0a911227ea2ecfcba9f672a77cf1924df5280c4562032ec32d/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d434ec7e2ce3cc8f452d1cd9a28591745de022f931d67be688a737320dfcead", size = 42857276, upload-time = "2024-06-13T18:08:21.071Z" }, + { url = "https://files.pythonhosted.org/packages/c6/21/2ffbab5714e72f2483207b4a1de79b2eecd9debbf666ff4e7067bcc5c134/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6912a87782acdff6eb8bf01675ed01d60ca1f2551f8176a300a886f09e836a6a", size = 43871781, upload-time = "2024-06-13T18:08:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/b5478037c453554a61625ef1125f7e12bb1429ae11c6376f47beba9b0179/llvmlite-0.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:14f0e4bf2fd2d9a75a3534111e8ebeb08eda2f33e9bdd6dfa13282afacdde0ed", size = 28123487, upload-time = "2024-06-13T18:08:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/de3276d773ab6ce3ad676df5fab5aac19696b2956319d65d7dd88fb10f19/llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8d0618cb9bfe40ac38a9633f2493d4d4e9fcc2f438d39a4e854f39cc0f5f98", size = 31064409, upload-time = "2024-06-13T18:08:34.006Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e1/38deed89ced4cf378c61e232265cfe933ccde56ae83c901aa68b477d14b1/llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0a9a1a39d4bf3517f2af9d23d479b4175ead205c592ceeb8b89af48a327ea57", size = 28793149, upload-time = "2024-06-13T18:08:37.42Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/4429433eb2dc8379e2cb582502dca074c23837f8fd009907f78a24de4c25/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1da416ab53e4f7f3bc8d4eeba36d801cc1894b9fbfbf2022b29b6bad34a7df2", size = 42857277, upload-time = "2024-06-13T18:08:40.822Z" }, + { url = "https://files.pythonhosted.org/packages/6b/99/5d00a7d671b1ba1751fc9f19d3b36f3300774c6eebe2bcdb5f6191763eb4/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977525a1e5f4059316b183fb4fd34fa858c9eade31f165427a3977c95e3ee749", size = 43871781, upload-time = "2024-06-13T18:08:46.41Z" }, + { url = "https://files.pythonhosted.org/packages/20/ab/ed5ed3688c6ba4f0b8d789da19fd8e30a9cf7fc5852effe311bc5aefe73e/llvmlite-0.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5bd550001d26450bd90777736c69d68c487d17bf371438f975229b2b8241a91", size = 28107433, upload-time = "2024-06-13T18:08:50.834Z" }, + { url = "https://files.pythonhosted.org/packages/0b/67/9443509e5d2b6d8587bae3ede5598fa8bd586b1c7701696663ea8af15b5b/llvmlite-0.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f99b600aa7f65235a5a05d0b9a9f31150c390f31261f2a0ba678e26823ec38f7", size = 31064409, upload-time = "2024-06-13T18:08:54.375Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9c/24139d3712d2d352e300c39c0e00d167472c08b3bd350c3c33d72c88ff8d/llvmlite-0.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:35d80d61d0cda2d767f72de99450766250560399edc309da16937b93d3b676e7", size = 28793145, upload-time = "2024-06-13T18:08:57.953Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/4c205a48488e574ee9f6505d50e84370a978c90f08dab41a42d8f2c576b6/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eccce86bba940bae0d8d48ed925f21dbb813519169246e2ab292b5092aba121f", size = 42857276, upload-time = "2024-06-13T18:09:02.067Z" }, + { url = "https://files.pythonhosted.org/packages/00/5f/323c4d56e8401c50185fd0e875fcf06b71bf825a863699be1eb10aa2a9cb/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6509e1507ca0760787a199d19439cc887bfd82226f5af746d6977bd9f66844", size = 43871781, upload-time = "2024-06-13T18:09:06.667Z" }, + { url = "https://files.pythonhosted.org/packages/c6/94/dea10e263655ce78d777e78d904903faae39d1fc440762be4a9dc46bed49/llvmlite-0.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a2872ee80dcf6b5dbdc838763d26554c2a18aa833d31a2635bff16aafefb9c9", size = 28107442, upload-time = "2024-06-13T18:09:10.709Z" }, + { url = "https://files.pythonhosted.org/packages/2a/73/12925b1bbb3c2beb6d96f892ef5b4d742c34f00ddb9f4a125e9e87b22f52/llvmlite-0.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cd2a7376f7b3367019b664c21f0c61766219faa3b03731113ead75107f3b66c", size = 31064410, upload-time = "2024-06-13T18:09:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/58c70aa0808a8cba825a7d98cc65bef4801b99328fba80837bfcb5fc767f/llvmlite-0.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18e9953c748b105668487b7c81a3e97b046d8abf95c4ddc0cd3c94f4e4651ae8", size = 28793145, upload-time = "2024-06-13T18:09:17.531Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c6/9324eb5de2ba9d99cbed853d85ba7a318652a48e077797bec27cf40f911d/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74937acd22dc11b33946b67dca7680e6d103d6e90eeaaaf932603bec6fe7b03a", size = 42857276, upload-time = "2024-06-13T18:09:21.377Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d0/889e9705107db7b1ec0767b03f15d7b95b4c4f9fdf91928ab1c7e9ffacf6/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9efc739cc6ed760f795806f67889923f7274276f0eb45092a1473e40d9b867", size = 43871777, upload-time = "2024-06-13T18:09:25.76Z" }, + { url = "https://files.pythonhosted.org/packages/df/41/73cc26a2634b538cfe813f618c91e7e9960b8c163f8f0c94a2b0f008b9da/llvmlite-0.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:47e147cdda9037f94b399bf03bfd8a6b6b1f2f90be94a454e3386f006455a9b4", size = 28123489, upload-time = "2024-06-13T18:09:29.78Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/a4/3959e1c61c5ca9db7921e5fd115b344c29b9d57a5dadd87bef97963ca1a5/llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0", size = 37232766, upload-time = "2025-12-08T18:14:34.765Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a5/a4d916f1015106e1da876028606a8e87fd5d5c840f98c87bc2d5153b6a2f/llvmlite-0.46.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a2d461cb89537b7c20feb04c46c32e12d5ad4f0896c9dfc0f60336219ff248e", size = 56275176, upload-time = "2025-12-08T18:14:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/79/7f/a7f2028805dac8c1a6fae7bda4e739b7ebbcd45b29e15bf6d21556fcd3d5/llvmlite-0.46.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1f6595a35b7b39c3518b85a28bf18f45e075264e4b2dce3f0c2a4f232b4a910", size = 55128629, upload-time = "2025-12-08T18:14:41.674Z" }, + { url = "https://files.pythonhosted.org/packages/b2/bc/4689e1ba0c073c196b594471eb21be0aa51d9e64b911728aa13cd85ef0ae/llvmlite-0.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7a34d4aa6f9a97ee006b504be6d2b8cb7f755b80ab2f344dda1ef992f828559", size = 38138651, upload-time = "2025-12-08T18:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a1/2ad4b2367915faeebe8447f0a057861f646dbf5fbbb3561db42c65659cf3/llvmlite-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82f3d39b16f19aa1a56d5fe625883a6ab600d5cc9ea8906cca70ce94cabba067", size = 37232766, upload-time = "2025-12-08T18:14:48.836Z" }, + { url = "https://files.pythonhosted.org/packages/12/b5/99cf8772fdd846c07da4fd70f07812a3c8fd17ea2409522c946bb0f2b277/llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3df43900119803bbc52720e758c76f316a9a0f34612a886862dfe0a5591a17e", size = 56275175, upload-time = "2025-12-08T18:14:51.604Z" }, + { url = "https://files.pythonhosted.org/packages/38/f2/ed806f9c003563732da156139c45d970ee435bd0bfa5ed8de87ba972b452/llvmlite-0.46.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de183fefc8022d21b0aa37fc3e90410bc3524aed8617f0ff76732fc6c3af5361", size = 55128630, upload-time = "2025-12-08T18:14:55.107Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/8f5a37a65fc9b7b17408508145edd5f86263ad69c19d3574e818f533a0eb/llvmlite-0.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8b10bc585c58bdffec9e0c309bb7d51be1f2f15e169a4b4d42f2389e431eb93", size = 38138652, upload-time = "2025-12-08T18:14:58.171Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" }, +] + +[[package]] +name = "markdown" +version = "3.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { 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/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.9.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "cycler", marker = "python_full_version < '3.10'" }, + { name = "fonttools", version = "4.60.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "importlib-resources", marker = "python_full_version < '3.10'" }, + { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyparsing", marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529, upload-time = "2024-12-13T05:56:34.184Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089, upload-time = "2024-12-13T05:54:24.224Z" }, + { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600, upload-time = "2024-12-13T05:54:27.214Z" }, + { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138, upload-time = "2024-12-13T05:54:29.497Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711, upload-time = "2024-12-13T05:54:34.396Z" }, + { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622, upload-time = "2024-12-13T05:54:36.808Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211, upload-time = "2024-12-13T05:54:40.596Z" }, + { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430, upload-time = "2024-12-13T05:54:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045, upload-time = "2024-12-13T05:54:46.414Z" }, + { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906, upload-time = "2024-12-13T05:54:49.459Z" }, + { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873, upload-time = "2024-12-13T05:54:53.066Z" }, + { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566, upload-time = "2024-12-13T05:54:55.522Z" }, + { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065, upload-time = "2024-12-13T05:54:58.337Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131, upload-time = "2024-12-13T05:55:02.837Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365, upload-time = "2024-12-13T05:55:05.158Z" }, + { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707, upload-time = "2024-12-13T05:55:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761, upload-time = "2024-12-13T05:55:12.95Z" }, + { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284, upload-time = "2024-12-13T05:55:16.199Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160, upload-time = "2024-12-13T05:55:19.991Z" }, + { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919, upload-time = "2024-12-13T05:55:59.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486, upload-time = "2024-12-13T05:56:04.264Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838, upload-time = "2024-12-13T05:56:06.792Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492, upload-time = "2024-12-13T05:56:09.964Z" }, + { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500, upload-time = "2024-12-13T05:56:13.55Z" }, + { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962, upload-time = "2024-12-13T05:56:16.358Z" }, + { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995, upload-time = "2024-12-13T05:56:18.805Z" }, + { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300, upload-time = "2024-12-13T05:56:21.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423, upload-time = "2024-12-13T05:56:26.719Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624, upload-time = "2024-12-13T05:56:29.359Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler", marker = "python_full_version >= '3.10'" }, + { name = "fonttools", version = "4.61.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "kiwisolver", version = "1.4.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyparsing", marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "mistune" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, +] + +[[package]] +name = "moreorless" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/85/2e4999ac4a21ab3c5f31e2a48e0989a80be3afc512a7983e3253615983d4/moreorless-0.5.0.tar.gz", hash = "sha256:560a04f85006fccd74feaa4b6213a446392ff7b5ec0194a5464b6c30f182fa33", size = 14093, upload-time = "2025-05-04T22:29:59.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/2e/9ea80ca55b73530b7639c6f146a58f636ddfe5a852ad467a44fe3e80d809/moreorless-0.5.0-py3-none-any.whl", hash = "sha256:66228870cd2f14bad5c3c3780aa71e29d3b2d9b5a01c03bfbf105efd4f668ecf", size = 14380, upload-time = "2025-05-04T22:29:57.417Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "jupyter-client", version = "8.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "nbformat", marker = "python_full_version < '3.10'" }, + { name = "traitlets", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424, upload-time = "2024-12-19T10:32:27.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434, upload-time = "2024-12-19T10:32:24.139Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "jupyter-client", version = "8.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "nbformat", marker = "python_full_version >= '3.10'" }, + { name = "traitlets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", version = "6.2.0", source = { registry = "https://pypi.org/simple" }, extra = ["css"], marker = "python_full_version < '3.10'" }, + { name = "bleach", version = "6.3.0", source = { registry = "https://pypi.org/simple" }, extra = ["css"], marker = "python_full_version >= '3.10'" }, + { name = "defusedxml" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient", version = "0.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "nbclient", version = "0.10.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jsonschema", version = "4.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/80/a84676339aaae2f1cfdf9f418701dd634aef9cc76f708ef55c36ff39c3ca/networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6", size = 2073928, upload-time = "2023-10-28T08:41:39.364Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/f0/8fbc882ca80cf077f1b246c0e3c3465f7f415439bdea6b899f6b19f61f70/networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2", size = 1647772, upload-time = "2023-10-28T08:41:36.945Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' 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 = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +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 = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "notebook" +version = "7.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, + { name = "jupyterlab" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/cb/cc7f4df5cee315dd126a47eb60890690a0438d5e0dd40c32d60ce16de377/notebook-7.5.3.tar.gz", hash = "sha256:393ceb269cf9fdb02a3be607a57d7bd5c2c14604f1818a17dbeb38e04f98cbfa", size = 14073140, upload-time = "2026-01-26T07:28:36.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/98/9286e7f35e5584ebb79f997f2fb0cb66745c86f6c5fccf15ba32aac5e908/notebook-7.5.3-py3-none-any.whl", hash = "sha256:c997bfa1a2a9eb58c9bbb7e77d50428befb1033dd6f02c482922e96851d67354", size = 14481744, upload-time = "2026-01-26T07:28:31.867Z" }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, +] + +[[package]] +name = "numba" +version = "0.60.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "llvmlite", version = "0.43.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/93/2849300a9184775ba274aba6f82f303343669b0592b7bb0849ea713dabb0/numba-0.60.0.tar.gz", hash = "sha256:5df6158e5584eece5fc83294b949fd30b9f1125df7708862205217e068aabf16", size = 2702171, upload-time = "2024-06-13T18:11:19.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/cf/baa13a7e3556d73d9e38021e6d6aa4aeb30d8b94545aa8b70d0f24a1ccc4/numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651", size = 2647627, upload-time = "2024-06-13T18:10:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/4b57fa498564457c3cc9fc9e570a6b08e6086c74220f24baaf04e54b995f/numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b", size = 2650322, upload-time = "2024-06-13T18:10:32.849Z" }, + { url = "https://files.pythonhosted.org/packages/28/98/7ea97ee75870a54f938a8c70f7e0be4495ba5349c5f9db09d467c4a5d5b7/numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1527dc578b95c7c4ff248792ec33d097ba6bef9eda466c948b68dfc995c25781", size = 3407390, upload-time = "2024-06-13T18:10:34.741Z" }, + { url = "https://files.pythonhosted.org/packages/79/58/cb4ac5b8f7ec64200460aef1fed88258fb872ceef504ab1f989d2ff0f684/numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe0b28abb8d70f8160798f4de9d486143200f34458d34c4a214114e445d7124e", size = 3699694, upload-time = "2024-06-13T18:10:37.295Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/c61a93ca947d12233ff45de506ddbf52af3f752066a0b8be4d27426e16da/numba-0.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:19407ced081d7e2e4b8d8c36aa57b7452e0283871c296e12d798852bc7d7f198", size = 2687030, upload-time = "2024-06-13T18:10:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/98/ad/df18d492a8f00d29a30db307904b9b296e37507034eedb523876f3a2e13e/numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a17b70fc9e380ee29c42717e8cc0bfaa5556c416d94f9aa96ba13acb41bdece8", size = 2647254, upload-time = "2024-06-13T18:10:41.69Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/a4dc2c01ce7a850b8e56ff6d5381d047a5daea83d12bad08aa071d34b2ee/numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fb02b344a2a80efa6f677aa5c40cd5dd452e1b35f8d1c2af0dfd9ada9978e4b", size = 2649970, upload-time = "2024-06-13T18:10:44.682Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/8889ac94c0b33dca80bed11564b8c6d9ea14d7f094e674c58e5c5b05859b/numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f4fde652ea604ea3c86508a3fb31556a6157b2c76c8b51b1d45eb40c8598703", size = 3412492, upload-time = "2024-06-13T18:10:47.1Z" }, + { url = "https://files.pythonhosted.org/packages/57/03/2b4245b05b71c0cee667e6a0b51606dfa7f4157c9093d71c6b208385a611/numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4142d7ac0210cc86432b818338a2bc368dc773a2f5cf1e32ff7c5b378bd63ee8", size = 3705018, upload-time = "2024-06-13T18:10:49.539Z" }, + { url = "https://files.pythonhosted.org/packages/79/89/2d924ca60dbf949f18a6fec223a2445f5f428d9a5f97a6b29c2122319015/numba-0.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cac02c041e9b5bc8cf8f2034ff6f0dbafccd1ae9590dc146b3a02a45e53af4e2", size = 2686920, upload-time = "2024-06-13T18:10:51.937Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5c/b5ec752c475e78a6c3676b67c514220dbde2725896bbb0b6ec6ea54b2738/numba-0.60.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7da4098db31182fc5ffe4bc42c6f24cd7d1cb8a14b59fd755bfee32e34b8404", size = 2647866, upload-time = "2024-06-13T18:10:54.453Z" }, + { url = "https://files.pythonhosted.org/packages/65/42/39559664b2e7c15689a638c2a38b3b74c6e69a04e2b3019b9f7742479188/numba-0.60.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38d6ea4c1f56417076ecf8fc327c831ae793282e0ff51080c5094cb726507b1c", size = 2650208, upload-time = "2024-06-13T18:10:56.779Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/c4459ccc05674ef02119abf2888ccd3e2fed12a323f52255f4982fc95876/numba-0.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62908d29fb6a3229c242e981ca27e32a6e606cc253fc9e8faeb0e48760de241e", size = 3466946, upload-time = "2024-06-13T18:10:58.961Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/ac11cf33524def12aa5bd698226ae196a1185831c05ed29dc0c56eaa308b/numba-0.60.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ebaa91538e996f708f1ab30ef4d3ddc344b64b5227b67a57aa74f401bb68b9d", size = 3761463, upload-time = "2024-06-13T18:11:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bd/0fe29fcd1b6a8de479a4ed25c6e56470e467e3611c079d55869ceef2b6d1/numba-0.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:f75262e8fe7fa96db1dca93d53a194a38c46da28b112b8a4aca168f0df860347", size = 2707588, upload-time = "2024-06-13T18:11:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/68/1a/87c53f836cdf557083248c3f47212271f220280ff766538795e77c8c6bbf/numba-0.60.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:01ef4cd7d83abe087d644eaa3d95831b777aa21d441a23703d649e06b8e06b74", size = 2647186, upload-time = "2024-06-13T18:11:06.753Z" }, + { url = "https://files.pythonhosted.org/packages/28/14/a5baa1f2edea7b49afa4dc1bb1b126645198cf1075186853b5b497be826e/numba-0.60.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:819a3dfd4630d95fd574036f99e47212a1af41cbcb019bf8afac63ff56834449", size = 2650038, upload-time = "2024-06-13T18:11:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bd/f1985719ff34e37e07bb18f9d3acd17e5a21da255f550c8eae031e2ddf5f/numba-0.60.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b983bd6ad82fe868493012487f34eae8bf7dd94654951404114f23c3466d34b", size = 3403010, upload-time = "2024-06-13T18:11:13.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/9b/cd73d3f6617ddc8398a63ef97d8dc9139a9879b9ca8a7ca4b8789056ea46/numba-0.60.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c151748cd269ddeab66334bd754817ffc0cabd9433acb0f551697e5151917d25", size = 3695086, upload-time = "2024-06-13T18:11:15.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/01/8b7b670c77c5ea0e47e283d82332969bf672ab6410d0b2610cac5b7a3ded/numba-0.60.0-cp39-cp39-win_amd64.whl", hash = "sha256:3031547a015710140e8c87226b4cfe927cac199835e5bf7d4fe5cb64e814e3ab", size = 2686978, upload-time = "2024-06-13T18:11:17.765Z" }, +] + +[[package]] +name = "numba" +version = "0.63.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "llvmlite", version = "0.46.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/ce/5283d4ffa568f795bb0fd61ee1f0efc0c6094b94209259167fc8d4276bde/numba-0.63.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6d6bf5bf00f7db629305caaec82a2ffb8abe2bf45eaad0d0738dc7de4113779", size = 2680810, upload-time = "2025-12-10T02:56:55.269Z" }, + { url = "https://files.pythonhosted.org/packages/0f/72/a8bda517e26d912633b32626333339b7c769ea73a5c688365ea5f88fd07e/numba-0.63.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08653d0dfc9cc9c4c9a8fba29ceb1f2d5340c3b86c4a7e5e07e42b643bc6a2f4", size = 3739735, upload-time = "2025-12-10T02:56:57.922Z" }, + { url = "https://files.pythonhosted.org/packages/ca/17/1913b7c1173b2db30fb7a9696892a7c4c59aeee777a9af6859e9e01bac51/numba-0.63.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09eebf5650246ce2a4e9a8d38270e2d4b0b0ae978103bafb38ed7adc5ea906e", size = 3446707, upload-time = "2025-12-10T02:56:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/b4/77/703db56c3061e9fdad5e79c91452947fdeb2ec0bdfe4affe9b144e7025e0/numba-0.63.1-cp310-cp310-win_amd64.whl", hash = "sha256:f8bba17421d865d8c0f7be2142754ebce53e009daba41c44cf6909207d1a8d7d", size = 2747374, upload-time = "2025-12-10T02:57:07.908Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/5f8614c165d2e256fbc6c57028519db6f32e4982475a372bbe550ea0454c/numba-0.63.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b33db00f18ccc790ee9911ce03fcdfe9d5124637d1ecc266f5ae0df06e02fec3", size = 2680501, upload-time = "2025-12-10T02:57:09.797Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9d/d0afc4cf915edd8eadd9b2ab5b696242886ee4f97720d9322650d66a88c6/numba-0.63.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d31ea186a78a7c0f6b1b2a3fe68057fdb291b045c52d86232b5383b6cf4fc25", size = 3744945, upload-time = "2025-12-10T02:57:11.697Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/d82f38f2ab73f3be6f838a826b545b80339762ee8969c16a8bf1d39395a8/numba-0.63.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed3bb2fbdb651d6aac394388130a7001aab6f4541837123a4b4ab8b02716530c", size = 3450827, upload-time = "2025-12-10T02:57:13.709Z" }, + { url = "https://files.pythonhosted.org/packages/18/3f/a9b106e93c5bd7434e65f044bae0d204e20aa7f7f85d72ceb872c7c04216/numba-0.63.1-cp311-cp311-win_amd64.whl", hash = "sha256:1ecbff7688f044b1601be70113e2fb1835367ee0b28ffa8f3adf3a05418c5c87", size = 2747262, upload-time = "2025-12-10T02:57:15.664Z" }, + { url = "https://files.pythonhosted.org/packages/14/9c/c0974cd3d00ff70d30e8ff90522ba5fbb2bcee168a867d2321d8d0457676/numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71", size = 2680981, upload-time = "2025-12-10T02:57:17.579Z" }, + { url = "https://files.pythonhosted.org/packages/cb/70/ea2bc45205f206b7a24ee68a159f5097c9ca7e6466806e7c213587e0c2b1/numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f", size = 3801656, upload-time = "2025-12-10T02:57:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/4f4ba4fd0f99825cbf3cdefd682ca3678be1702b63362011de6e5f71f831/numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0", size = 3501857, upload-time = "2025-12-10T02:57:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/af/fd/6540456efa90b5f6604a86ff50dabefb187e43557e9081adcad3be44f048/numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3", size = 2750282, upload-time = "2025-12-10T02:57:22.474Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, + { url = "https://files.pythonhosted.org/packages/7d/24/ce71dc08f06534269f66e73c04f5709ee024a1afe92a7b6e1d73f158e1f8/numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c", size = 20636301, upload-time = "2024-02-05T23:59:10.976Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/ab03a7c25741f9ebc92684a20125fbc9fc1b8e1e700beb9197d750fdff88/numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be", size = 13971216, upload-time = "2024-02-05T23:59:35.472Z" }, + { url = "https://files.pythonhosted.org/packages/6d/64/c3bcdf822269421d85fe0d64ba972003f9bb4aa9a419da64b86856c9961f/numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764", size = 14226281, upload-time = "2024-02-05T23:59:59.372Z" }, + { url = "https://files.pythonhosted.org/packages/54/30/c2a907b9443cf42b90c17ad10c1e8fa801975f01cb9764f3f8eb8aea638b/numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", size = 18249516, upload-time = "2024-02-06T00:00:32.79Z" }, + { url = "https://files.pythonhosted.org/packages/43/12/01a563fc44c07095996d0129b8899daf89e4742146f7044cdbdb3101c57f/numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd", size = 13882132, upload-time = "2024-02-06T00:00:58.197Z" }, + { url = "https://files.pythonhosted.org/packages/16/ee/9df80b06680aaa23fc6c31211387e0db349e0e36d6a63ba3bd78c5acdf11/numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c", size = 18084181, upload-time = "2024-02-06T00:01:31.21Z" }, + { url = "https://files.pythonhosted.org/packages/28/7d/4b92e2fe20b214ffca36107f1a3e75ef4c488430e64de2d9af5db3a4637d/numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6", size = 5976360, upload-time = "2024-02-06T00:01:43.013Z" }, + { url = "https://files.pythonhosted.org/packages/b5/42/054082bd8220bbf6f297f982f0a8f5479fcbc55c8b511d928df07b965869/numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea", size = 15814633, upload-time = "2024-02-06T00:02:16.694Z" }, + { url = "https://files.pythonhosted.org/packages/3f/72/3df6c1c06fc83d9cfe381cccb4be2532bbd38bf93fbc9fad087b6687f1c0/numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30", size = 20455961, upload-time = "2024-02-06T00:03:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/570545bac308b58ffb21adda0f4e220ba716fb658a63c151daecc3293350/numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c", size = 18061071, upload-time = "2024-02-06T00:03:41.5Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/fafd8c51235f60d49f7a88e2275e13971e90555b67da52dd6416caec32fe/numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0", size = 15709730, upload-time = "2024-02-06T00:04:11.719Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.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" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +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" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +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" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +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" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, +] +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" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, +] +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" +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" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +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" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "nvidia-cusparse-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, +] +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" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, +] +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" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/5b/4e4fff7bad39adf89f735f2bc87248c81db71205b62bcc0d5ca5b606b3c3/nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adf27ccf4238253e0b826bce3ff5fa532d65fc42322c8bfdfaf28024c0fbe039", size = 322364134, upload-time = "2025-06-03T21:58:04.013Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +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" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +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" +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" }, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.11.0.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322, upload-time = "2025-01-16T13:52:25.887Z" }, + { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197, upload-time = "2025-01-16T13:55:21.222Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439, upload-time = "2025-01-16T13:51:35.822Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597, upload-time = "2025-01-16T13:52:08.836Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337, upload-time = "2025-01-16T13:52:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/56/b4/52eeb530a99e2a4c55ffcd352772b599ed4473a0f892d127f4147cf0f88e/pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2", size = 11567720, upload-time = "2025-09-29T23:33:06.209Z" }, + { url = "https://files.pythonhosted.org/packages/48/4a/2d8b67632a021bced649ba940455ed441ca854e57d6e7658a6024587b083/pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8", size = 10810302, upload-time = "2025-09-29T23:33:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/d2465010ee0569a245c975dc6967b801887068bc893e908239b1f4b6c1ac/pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff", size = 12154874, upload-time = "2025-09-29T23:33:49.939Z" }, + { url = "https://files.pythonhosted.org/packages/1f/18/aae8c0aa69a386a3255940e9317f793808ea79d0a525a97a903366bb2569/pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29", size = 12790141, upload-time = "2025-09-29T23:34:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/f7/26/617f98de789de00c2a444fbe6301bb19e66556ac78cff933d2c98f62f2b4/pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73", size = 13208697, upload-time = "2025-09-29T23:34:21.835Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fb/25709afa4552042bd0e15717c75e9b4a2294c3dc4f7e6ea50f03c5136600/pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9", size = 13879233, upload-time = "2025-09-29T23:34:35.079Z" }, + { url = "https://files.pythonhosted.org/packages/98/af/7be05277859a7bc399da8ba68b88c96b27b48740b6cf49688899c6eb4176/pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa", size = 11359119, upload-time = "2025-09-29T23:34:46.339Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/1e/b184654a856e75e975a6ee95d6577b51c271cd92cb2b020c9378f53e0032/pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", size = 10313247, upload-time = "2026-01-21T15:50:15.775Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/e04a547ad0f0183bf151fd7c7a477468e3b85ff2ad231c566389e6cc9587/pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", size = 9913131, upload-time = "2026-01-21T15:50:18.611Z" }, + { url = "https://files.pythonhosted.org/packages/a2/93/bb77bfa9fc2aba9f7204db807d5d3fb69832ed2854c60ba91b4c65ba9219/pandas-3.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", size = 10741925, upload-time = "2026-01-21T15:50:21.058Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/89319812eb1d714bfc04b7f177895caeba8ab4a37ef6712db75ed786e2e0/pandas-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", size = 11245979, upload-time = "2026-01-21T15:50:23.413Z" }, + { url = "https://files.pythonhosted.org/packages/a9/63/684120486f541fc88da3862ed31165b3b3e12b6a1c7b93be4597bc84e26c/pandas-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", size = 11756337, upload-time = "2026-01-21T15:50:25.932Z" }, + { url = "https://files.pythonhosted.org/packages/39/92/7eb0ad232312b59aec61550c3c81ad0743898d10af5df7f80bc5e5065416/pandas-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", size = 12325517, upload-time = "2026-01-21T15:50:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/51/27/bf9436dd0a4fc3130acec0828951c7ef96a0631969613a9a35744baf27f6/pandas-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", size = 9881576, upload-time = "2026-01-21T15:50:30.149Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/c618b871fce0159fd107516336e82891b404e3f340821853c2fc28c7830f/pandas-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", size = 9140807, upload-time = "2026-01-21T15:50:32.308Z" }, + { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478, upload-time = "2025-07-01T09:15:52.209Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522, upload-time = "2025-07-01T09:15:54.162Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376, upload-time = "2025-07-03T13:11:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020, upload-time = "2025-07-03T13:11:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732, upload-time = "2025-07-01T09:15:56.111Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404, upload-time = "2025-07-01T09:15:58.245Z" }, + { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760, upload-time = "2025-07-01T09:16:00.003Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534, upload-time = "2025-07-01T09:16:02.29Z" }, + { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091, upload-time = "2025-07-01T09:16:04.4Z" }, + { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091, upload-time = "2025-07-01T09:16:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632, upload-time = "2025-07-01T09:16:08.142Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, + { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" }, + { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, + { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, + { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, + { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "portalocker" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/84d5f6dcda9165e4d6a56ac8433c9f40a8906bf2966150b8a0cfde097d78/protobuf-6.33.5-cp39-cp39-win32.whl", hash = "sha256:a3157e62729aafb8df6da2c03aa5c0937c7266c626ce11a278b6eb7963c4e37c", size = 425892, upload-time = "2026-01-29T21:51:30.382Z" }, + { url = "https://files.pythonhosted.org/packages/68/19/33d7dc2dc84439587fa1e21e1c0026c01ad2af0a62f58fd54002a7546307/protobuf-6.33.5-cp39-cp39-win_amd64.whl", hash = "sha256:8f04fa32763dcdb4973d537d6b54e615cc61108c7cb38fe59310c3192d29510a", size = 437137, upload-time = "2026-01-29T21:51:31.456Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pycocotools" +version = "2.0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/df/32354b5dda963ffdfc8f75c9acf8828ef7890723a4ed57bb3ff2dc1d6f7e/pycocotools-2.0.11.tar.gz", hash = "sha256:34254d76da85576fcaf5c1f3aa9aae16b8cb15418334ba4283b800796bd1993d", size = 25381, upload-time = "2025-12-15T22:31:46.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/4b/0c040fcda2c4fa4827b1a64e3185d99d5f954e45cc9463ba7385a1173a77/pycocotools-2.0.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:484d33515353186aadba9e2a290d81b107275cdb9565084e31a5568a52a0b120", size = 160351, upload-time = "2025-12-15T22:30:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/49/fe/861db6515824815eaabce27734653a6b100ddb22364b3345dd862b2c5b65/pycocotools-2.0.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca9f120f719ec405ad0c74ccfdb8402b0c37bd5f88ab5b6482a0de2efd5a36f4", size = 463947, upload-time = "2025-12-15T22:30:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a1/b4b49b85763043372e66baa10dffa42337cf4687d6db22546c27f3a4d732/pycocotools-2.0.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e40a3a898c6e5340b8d70cf7984868b9bff8c3d80187de9a3b661d504d665978", size = 472455, upload-time = "2025-12-15T22:30:56.895Z" }, + { url = "https://files.pythonhosted.org/packages/48/70/fac670296e6a2b45eb7434d0480b9af6cb85a8de4f4848b49b01154bc859/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7cd4cdfd2c676f30838aa0b1047441892fb4f97d70bf3df480bcc7a18a64d7d4", size = 457911, upload-time = "2025-12-15T22:30:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/33/f5/6158de63354dfcb677c8da34a4d205cc532e3277338ab7e6dea1310ba8de/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08c79789fd79e801ae4ecfcfeec32b31e36254e7a2b4019af28c104975d5e730", size = 476472, upload-time = "2025-12-15T22:30:59.736Z" }, + { url = "https://files.pythonhosted.org/packages/fc/01/46d2a782cda19ba1beb7c431f417e1e478f0bf1273fa5fe5d10de7c18d76/pycocotools-2.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:f78cbb1a32d061fcad4bdba083de70a39a21c1c3d9235a3f77d8f007541ec5ef", size = 80165, upload-time = "2025-12-15T22:31:00.886Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5c/6bd945781bb04c2148929183d1d67b05ce07996313b0f87bb88c6a805493/pycocotools-2.0.11-cp310-cp310-win_arm64.whl", hash = "sha256:e21311ea71f85591680d8992858e2d44a2a156dc3b2bf1c5c901c4a19348177b", size = 69358, upload-time = "2025-12-15T22:31:01.815Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/41ce3fce61b7721158f21b61727eb054805babc0088cfa48506935b80a36/pycocotools-2.0.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:81bdceebb4c64e9265213e2d733808a12f9c18dfb14457323cc6b9af07fa0e61", size = 158947, upload-time = "2025-12-15T22:31:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9b/a739705b246445bd1376394bf9d1ec2dd292b16740e92f203461b2bb12ed/pycocotools-2.0.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c05f91ccc658dfe01325267209c4b435da1722c93eeb5749fabc1d087b6882", size = 485174, upload-time = "2025-12-15T22:31:04.395Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/7a12752784e57d8034a76c245c618a2f88a9d2463862b990f314aea7e5d6/pycocotools-2.0.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18ba75ff58cedb33a85ce2c18f1452f1fe20c9dd59925eec5300b2bf6205dbe1", size = 493172, upload-time = "2025-12-15T22:31:05.504Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fc/d703599ac728209dba08aea8d4bee884d5adabfcd9041abed1658d863747/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:693417797f0377fd094eb815c0a1e7d1c3c0251b71e3b3779fce3b3cf24793c5", size = 480506, upload-time = "2025-12-15T22:31:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/d9/e1cfc320bbb2cd58c3b4398c3821cbe75d93c16ed3135ac9e774a18a02d3/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6a07071c441d0f5e480a8f287106191582e40289d4e242dfe684e0c8a751088", size = 497595, upload-time = "2025-12-15T22:31:08.277Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/d17f6111c2a6ae8631d4fa90202bea05844da715d61431fbc34d276462d5/pycocotools-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:8e159232adae3aef6b4e2d37b008bff107b26e9ed3b48e70ea6482302834bd34", size = 80519, upload-time = "2025-12-15T22:31:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/00/4c/76b00b31a724c3f5ccdab0f85e578afb2ca38d33be0a0e98f1770cafd958/pycocotools-2.0.11-cp311-cp311-win_arm64.whl", hash = "sha256:4fc9889e819452b9c142036e1eabac8a13a8bd552d8beba299a57e0da6bfa1ec", size = 69304, upload-time = "2025-12-15T22:31:10.592Z" }, + { url = "https://files.pythonhosted.org/packages/87/12/2f2292332456e4e4aba1dec0e3de8f1fc40fb2f4fdb0ca1cb17db9861682/pycocotools-2.0.11-cp312-abi3-macosx_10_13_universal2.whl", hash = "sha256:a2e9634bc7cadfb01c88e0b98589aaf0bd12983c7927bde93f19c0103e5441f4", size = 147795, upload-time = "2025-12-15T22:31:11.519Z" }, + { url = "https://files.pythonhosted.org/packages/63/3c/68d7ea376aada9046e7ea2d7d0dad0d27e1ae8b4b3c26a28346689390ab2/pycocotools-2.0.11-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fd4121766cc057133534679c0ec3f9023dbd96e9b31cf95c86a069ebdac2b65", size = 398434, upload-time = "2025-12-15T22:31:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/dc81895beff4e1207a829d40d442ea87cefaac9f6499151965f05c479619/pycocotools-2.0.11-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a82d1c9ed83f75da0b3f244f2a3cf559351a283307bd9b79a4ee2b93ab3231dd", size = 411685, upload-time = "2025-12-15T22:31:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0b/5a8a7de300862a2eb5e2ecd3cb015126231379206cd3ebba8f025388d770/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89e853425018e2c2920ee0f2112cf7c140a1dcf5f4f49abd9c2da112c3e0f4b3", size = 390500, upload-time = "2025-12-15T22:31:15.138Z" }, + { url = "https://files.pythonhosted.org/packages/63/b5/519bb68647f06feea03d5f355c33c05800aeae4e57b9482b2859eb00752e/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87af87b8d06d5b852a885a319d9362dca3bed9f8bbcc3feb6513acb1f88ea242", size = 409790, upload-time = "2025-12-15T22:31:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/83/b4/f6708404ff494706b80e714b919f76dc4ec9845a4007affd6d6b0843f928/pycocotools-2.0.11-cp312-abi3-win_amd64.whl", hash = "sha256:ffe806ce535f5996445188f9a35643791dc54beabc61bd81e2b03367356d604f", size = 77570, upload-time = "2025-12-15T22:31:17.703Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/778cd0ddc9d4a78915ac0a72b56d7fb204f7c3fabdad067d67ea0089762e/pycocotools-2.0.11-cp312-abi3-win_arm64.whl", hash = "sha256:c230f5e7b14bd19085217b4f40bba81bf14a182b150b8e9fab1c15d504ade343", size = 64564, upload-time = "2025-12-15T22:31:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8a/e005c50aef072bb3800b8f38d77dd6d9a08f91efb17b65cffa7e8eaa3abc/pycocotools-2.0.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9dc8b388984c72aa84b1a68933f196ce71ab114c59232d0eab20c97cc1300875", size = 161170, upload-time = "2025-12-15T22:31:38.08Z" }, + { url = "https://files.pythonhosted.org/packages/04/59/d14d3adcf0dbe63774d80717bf865fc3b77822ed41ab7ae308c48192024d/pycocotools-2.0.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2b018497ec198ffc737dd7e6306a2e69999779ca619a9e12950e4506e410c3e", size = 461544, upload-time = "2025-12-15T22:31:39.151Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3c/a617217c3a5d8313ea656ded7f38ae34f78ad95be44bba12890faf389c6b/pycocotools-2.0.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e72527fdc00985d29dbe31d17b19cd2d16fbad7b01e974c567b593d5844de710", size = 470263, upload-time = "2025-12-15T22:31:40.429Z" }, + { url = "https://files.pythonhosted.org/packages/60/04/cc6dd61b27c58bed41fc0efc08e1405147c9146542b5955db5915a52a0e9/pycocotools-2.0.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d6b88557166794a24acd03f50296f2b95adf3e4206b4b8995e6bdca925c9cb72", size = 456222, upload-time = "2025-12-15T22:31:41.576Z" }, + { url = "https://files.pythonhosted.org/packages/21/6f/7db99a9faae1d1fa8ad9b9ca528b1144e76c849eb5c037bb0f8bda665320/pycocotools-2.0.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6a2f4f036f5bfdaf8c9625279051e921721fc9d27f92c97a3f3355a07ba38513", size = 475143, upload-time = "2025-12-15T22:31:42.822Z" }, + { url = "https://files.pythonhosted.org/packages/ea/64/7e3023ee979ce6619c053c3c852aafea6f8448d67c9b047f9233ce179cd0/pycocotools-2.0.11-cp39-cp39-win_amd64.whl", hash = "sha256:eba35d6e06caaea28ce65a4b92e4343ecbb9abf4915f7ef7fca989b80839111c", size = 80520, upload-time = "2025-12-15T22:31:44.216Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8b/f2a7a8fbae6bef8ea12428ecca6c208366a3ba79b2b76812cd6853f10f1b/pycocotools-2.0.11-cp39-cp39-win_arm64.whl", hash = "sha256:1192de413a23b4b94199197e8f8dbe1277cc24e6e9847bee6a71be3d8e543963", size = 69573, upload-time = "2025-12-15T22:31:45.189Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.13.3", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pluggy" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + +[[package]] +name = "python-rapidjson" +version = "1.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/3a/c32aee1dc385e50c1d6e78e56abdbc6aca283127f06f6ec0be1a86b2e3c1/python_rapidjson-1.23.tar.gz", hash = "sha256:0f845daeb26be147f5720a8c410308235092bb4fbb81ea408aa77203e26296fb", size = 239605, upload-time = "2025-12-07T06:14:27.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/df/653bedef7af1137d015501eb00cb2e1a46964015b3bb5c4e8096451af577/python_rapidjson-1.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbb0958c5132d3def9d2be178dee45a2c41071573558348091bc539fe2671cd2", size = 215755, upload-time = "2025-12-07T07:18:44.814Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5a/7e3f00667b949dff400e3ea36f2df4b0c7c9d12016820d2ad8289a9c2c05/python_rapidjson-1.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f9314d155f342bc95912a0db9c1087a8d043ecceccaff33c21d257de4376f20", size = 212834, upload-time = "2025-12-07T07:18:46.098Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/c3dfca7a9d1c2166bb1a74c786049c730a7534d480c11772802c6a6955e5/python_rapidjson-1.23-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:332c972ceaaa7faff559d370702682ed20bbe36fdab477679b157a0f3c0391d5", size = 1689856, upload-time = "2025-12-07T07:18:47.368Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f9/3101f069dbf95b64a1ce5d04a88161a653f7542a1312604b6361a2837610/python_rapidjson-1.23-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f92eed9120b83acd7be9df932318de1c0d6e40d01594e9d855fef41cdb55c7", size = 1749875, upload-time = "2025-12-07T07:18:48.599Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f3/87fb25da840ba08c08ca39755a3c2ef3cec74969436fb2e216f937e98a21/python_rapidjson-1.23-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b79f9a4af92188098008623b17743bf8a300abeacb383d0424d070b4fbfbf83e", size = 1727398, upload-time = "2025-12-07T07:18:50.627Z" }, + { url = "https://files.pythonhosted.org/packages/bc/94/959517668e91294cd5881d2dfa22b78602bf39225889f76dd98d003725f7/python_rapidjson-1.23-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e60f8aa275d403a407abeb31a6bf319826c9f6b565a569a8dfb930fe0a2cca1a", size = 2534620, upload-time = "2025-12-07T07:18:52.285Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/b2f08b3bbf16f30ddcd63de63cdb3b2306a6f2795d13fbf66dcaf1966352/python_rapidjson-1.23-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4c871e878c30f87077249d65ed65c0d5c24dd93c412f487cd95f648bec209909", size = 2665485, upload-time = "2025-12-07T07:18:53.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fd/5cae94b4b0615a479ac2644485c9350f83bcf522d975c40e3b757b590e67/python_rapidjson-1.23-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fc2b0c06b8e25528bbdeef45fda1ea373f62c277eb8f77518646e82ecc9b0718", size = 2648031, upload-time = "2025-12-07T07:18:55.568Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/3eba493d0b83d00752d6b6b4a158088888e6bc377e3621b8d895ab25fc44/python_rapidjson-1.23-cp310-cp310-win32.whl", hash = "sha256:027c2bd096ac505c52a7ff1a7f2860b0ee451bc75e69777b49fd3d842efff544", size = 130264, upload-time = "2025-12-07T07:18:57.021Z" }, + { url = "https://files.pythonhosted.org/packages/a5/28/01844118ab8689ea65c83f3c7008bab358e0110decf350d4aab9177863c6/python_rapidjson-1.23-cp310-cp310-win_amd64.whl", hash = "sha256:6516b8538b2081bbaf737326dba472b354d1bf873adbb155fc7c735916761afb", size = 150862, upload-time = "2025-12-07T07:18:58.35Z" }, + { url = "https://files.pythonhosted.org/packages/d8/aa/f2252ef867c46d92d9ecf7af0b461993be3900be2e4ac4d7952c5c2541f2/python_rapidjson-1.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6da3f5726c4ca30595b9ccab0e0bcfc18b624c61af5f6e3877b80e00bef62995", size = 215753, upload-time = "2025-12-07T07:18:59.636Z" }, + { url = "https://files.pythonhosted.org/packages/c4/95/7df68969ef598f32c0ee187712a215666cf382e1e85bcd8855efb253ac89/python_rapidjson-1.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:316e1541c98af3af4bc0908cb9baaa44019dcedcad69a9944e2237607deb3ce5", size = 212838, upload-time = "2025-12-07T07:19:00.645Z" }, + { url = "https://files.pythonhosted.org/packages/93/af/26ea25c2bf5c5c04aba0d66eba1ce1dd7f5dac0c3203d4ceb72617a99b20/python_rapidjson-1.23-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e771315f7abd9c345c95f82dc013b6d60f041b6d708b26585a84e011dd5e092d", size = 1708625, upload-time = "2025-12-07T07:19:02.055Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ae/ea28dee1cce61f06768b7ea56c1644a08f5373acb1c660ff8ab186688d77/python_rapidjson-1.23-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ea4f914033440f4474931838b93890567f9214e26dd89d741b4260d1e243e56", size = 1768734, upload-time = "2025-12-07T07:19:03.541Z" }, + { url = "https://files.pythonhosted.org/packages/99/8f/fb06132f7dc816b9689d43294bf58dd979da702cbdfe9fb265c5e7d54e6f/python_rapidjson-1.23-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0702eae42704948320df851a35371db3e7a9494c123896cbb069cc8b56e3c4e", size = 1745736, upload-time = "2025-12-07T07:19:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/0a/2f/4a7f28d170c5c2498f18eba3fa2781c597bf10832898bca7a1089e0e252a/python_rapidjson-1.23-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0cdc820cf2c7b5f4ac5ba4d697f049e0b93d2e9776b53b02a77051472d38cede", size = 2553385, upload-time = "2025-12-07T07:19:06.087Z" }, + { url = "https://files.pythonhosted.org/packages/8a/41/dfc3d019fde28479a48bd9783f69b24a9e38f011688f9f54a157442053bb/python_rapidjson-1.23-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:016a35f2d93ee6be13c938ab4bc30dcf525c4ee6e4c3fbd5c75c2320fccea875", size = 2682591, upload-time = "2025-12-07T07:19:07.325Z" }, + { url = "https://files.pythonhosted.org/packages/a9/46/d0f4edf6fef6ae6e544f823d1d4baa35d5e0e940c6485899ddb0577a7ceb/python_rapidjson-1.23-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40990adbf47bcb7f80b96b586e4ce114b497c19516a8e6a0700d61447ca8d28b", size = 2663035, upload-time = "2025-12-07T07:19:08.673Z" }, + { url = "https://files.pythonhosted.org/packages/39/4e/63a50c0ec7838da2997d3743104bf85f8c3444391ad413aca270ab93f2d9/python_rapidjson-1.23-cp311-cp311-win32.whl", hash = "sha256:8a2dc5faba744b643901489e82f037cef099be92b9d4d0eea597c1d5aea910be", size = 130259, upload-time = "2025-12-07T07:19:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/40/18/2c2836d38b0b19bbad406b1e3a138c8b28880a4f858ba008ad7ff31f1935/python_rapidjson-1.23-cp311-cp311-win_amd64.whl", hash = "sha256:6d2db055ed9728071117a8395ebb1552b937c3d5bbcf6f610420f9b6f926c654", size = 150796, upload-time = "2025-12-07T07:19:11.295Z" }, + { url = "https://files.pythonhosted.org/packages/08/e0/a78486cfb25a8c65d5e2a947aaa000bfd211b4705dc4e0657a42c6385cc5/python_rapidjson-1.23-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56e557fb6a7d7babfeb8ebaa4d096d4ce127477ecf46fe7de7f1edf2e1d8e4d6", size = 216508, upload-time = "2025-12-07T07:19:12.614Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f2/b8d9a47cf55e25d76865d7f1691b2b94b38061c5f3fa4b385848a362366e/python_rapidjson-1.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d8e107121f5c1e98cb4f0e5fde443e0f66b45eadc3269bc2416e31261535f444", size = 213921, upload-time = "2025-12-07T07:19:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ae/700b6f039fa799c3690193424185b1a2f1a49b035dd8cf81b73406dfbfca/python_rapidjson-1.23-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc45ef1f725b3a9a27cdedcf9997f1f8c5a523ac03882d3925c6f764b33e5e1b", size = 1722258, upload-time = "2025-12-07T07:19:15.249Z" }, + { url = "https://files.pythonhosted.org/packages/95/89/b4d2308a065d9a5ff3afc5c93c21358b5d82f944bbed4e54847231e24f81/python_rapidjson-1.23-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f87de7b994d65da2327fffdc5d3d7166782e3ca99c76c0560c8a7f1e109a5b54", size = 1780680, upload-time = "2025-12-07T07:19:16.71Z" }, + { url = "https://files.pythonhosted.org/packages/61/89/7b0047dfaa014cc456b29cf66913143bd0541225defaacf1727eee13291e/python_rapidjson-1.23-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6067810f0fd57713ec733b0b6ae265ef169e13b2ce04a4938b1807cddd8b4db4", size = 1760351, upload-time = "2025-12-07T07:19:17.946Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/a2dfb056a3ad6ca07c049c9376cfa509648765e805d9588c0f48bb998c33/python_rapidjson-1.23-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:83306643cf31c0833b226d4317e8738b1b5ed4371e310f3c552be994c01a3df0", size = 2570107, upload-time = "2025-12-07T07:19:19.17Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/e8873f34a07a524f4cb87a8934c783207674d5587533a50d0f2c55064d7b/python_rapidjson-1.23-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:13797fdcd43e558b81d3344c637bf878878fd6dede84409769d6910f8f6a9024", size = 2696763, upload-time = "2025-12-07T07:19:21.01Z" }, + { url = "https://files.pythonhosted.org/packages/23/cb/ad2a16d6b20a457e8acd745dca416f19cf0de738311d213c544112260cc8/python_rapidjson-1.23-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad674edb9dfe8181fb704a14149e5eb30ae179a92021484ebe8935b8d0f88495", size = 2675144, upload-time = "2025-12-07T07:19:22.609Z" }, + { url = "https://files.pythonhosted.org/packages/65/27/943fef83837f002d990274b82d5193d066aeef128c2ba6c009d549d0e5ad/python_rapidjson-1.23-cp312-cp312-win32.whl", hash = "sha256:0c64958048ce714ccc42c659ef954812ed6de79fe4800322b3926ca46f60ffd9", size = 130858, upload-time = "2025-12-07T07:19:23.887Z" }, + { url = "https://files.pythonhosted.org/packages/89/cd/ef6c1bc784c3a081fabcf867c1b3affcb18ba1ffd9d71aa036f96a2ef979/python_rapidjson-1.23-cp312-cp312-win_amd64.whl", hash = "sha256:cbb0a67a5330d28279a5c3b68068e901deedcd21ade0ec23be1bcc250948ae62", size = 151270, upload-time = "2025-12-07T07:19:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/10/d9/eae90129ee6c005887f2b1f1c14d1e4a75a1fcdad162c19c4c49690822f2/python_rapidjson-1.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:938d1bd7448151732aa7c70b251dbe9b7b3246a1b8b80c4110e86903e450a5f9", size = 215699, upload-time = "2025-12-07T07:20:07.851Z" }, + { url = "https://files.pythonhosted.org/packages/49/52/5735a53134d00242b747670d60493e3f0daea8eb1aae92561eda94108572/python_rapidjson-1.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:023ad77d11fd8023c03a038a4dbf734d6d54b74d9fa41414d93d0f696a77437d", size = 213043, upload-time = "2025-12-07T07:20:08.914Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/82b6f3c1e190e847ef42f721c564de9d94c7c068ad3caa8fce9aa5021119/python_rapidjson-1.23-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fe29411bbb63a15ea8c0003cbc82c185a4cf9602527d0fb026ccda10e440893", size = 1688385, upload-time = "2025-12-07T07:20:10.301Z" }, + { url = "https://files.pythonhosted.org/packages/d2/84/8cf37410060da7e481d64452a376d9fac3d910935a520901a5fb4f1b7237/python_rapidjson-1.23-cp39-cp39-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b90c2e51b3050062df91992c5fcdaf89def37450c41b067639a19d9aaa5fc85e", size = 1744729, upload-time = "2025-12-07T07:20:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/1da7c2a9f5d6f9c840dd20af6f5410d30b67ddd3d860570820494be19a44/python_rapidjson-1.23-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8956da1444a45ee5c4596b1574c50d6dc09a0874f388b975b726ddd0c4068c16", size = 1724906, upload-time = "2025-12-07T07:20:13.597Z" }, + { url = "https://files.pythonhosted.org/packages/df/49/977ba93cd3a19e9b0ea13143607398646a970878a9835eae35949084ad3f/python_rapidjson-1.23-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:29270878291beb42b6ac9abead70323780a44494d3451f820ffd81b3812f603c", size = 2532845, upload-time = "2025-12-07T07:20:15.129Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/51cecda13f501d5c1de0b4531884e18622c32c5ddb32f4d5a409b8131a56/python_rapidjson-1.23-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:63c072656336a0046a4176dd2a961dc26fe81a80c39c5908561820763fe3e632", size = 2663792, upload-time = "2025-12-07T07:20:16.823Z" }, + { url = "https://files.pythonhosted.org/packages/75/c0/772fbb4a677b240446b70d73e4cfc4e93e37bae6d613f0d00939b766af88/python_rapidjson-1.23-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:71b0d9eb4529610dec6b23d21056be5a6d3fe143bef177f8434df3a647774716", size = 2645035, upload-time = "2025-12-07T07:20:18.152Z" }, + { url = "https://files.pythonhosted.org/packages/54/37/6499427f4f77ff5c7a4bc0237093b37cef7e8c1ccd0dd29a3292cb294894/python_rapidjson-1.23-cp39-cp39-win32.whl", hash = "sha256:f0ceef3cd234754cc224ef73fcd1e9faa88ea278c5c1a578301ab31816345243", size = 129751, upload-time = "2025-12-07T07:20:20.829Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/4c1f9d9a93a0d772c8ba3248a1c5fc67a10badcadced9e965c5c9a25e737/python_rapidjson-1.23-cp39-cp39-win_amd64.whl", hash = "sha256:d50ba0791f5b2d6525cc6d2bbc9f4dca5d9991271ca57c6d1a82d782ce67e31b", size = 151236, upload-time = "2025-12-07T07:20:22.253Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, +] + +[[package]] +name = "pywinpty" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/54/37c7370ba91f579235049dc26cd2c5e657d2a943e01820844ffc81f32176/pywinpty-3.0.3.tar.gz", hash = "sha256:523441dc34d231fb361b4b00f8c99d3f16de02f5005fd544a0183112bcc22412", size = 31309, upload-time = "2026-02-04T21:51:09.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/28/a652709bd76ca7533cd1c443b03add9f5051fdf71bc6bdb8801dddd4e7a3/pywinpty-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:ff05f12d775b142b11c6fe085129bdd759b61cf7d41da6c745e78e3a1ef5bf40", size = 2114320, upload-time = "2026-02-04T21:53:50.972Z" }, + { url = "https://files.pythonhosted.org/packages/b2/13/a0181cc5c2d5635d3dbc3802b97bc8e3ad4fa7502ccef576651a5e08e54c/pywinpty-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:340ccacb4d74278a631923794ccd758471cfc8eeeeee4610b280420a17ad1e82", size = 235670, upload-time = "2026-02-04T21:50:20.324Z" }, + { url = "https://files.pythonhosted.org/packages/79/c3/3e75075c7f71735f22b66fab0481f2c98e3a4d58cba55cb50ba29114bcf6/pywinpty-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:dff25a9a6435f527d7c65608a7e62783fc12076e7d44487a4911ee91be5a8ac8", size = 2114430, upload-time = "2026-02-04T21:54:19.485Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1e/8a54166a8c5e4f5cb516514bdf4090be4d51a71e8d9f6d98c0aa00fe45d4/pywinpty-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc1e230e5b193eef4431cba3f39996a288f9958f9c9f092c8a961d930ee8f68", size = 236191, upload-time = "2026-02-04T21:50:36.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d4/aeb5e1784d2c5bff6e189138a9ca91a090117459cea0c30378e1f2db3d54/pywinpty-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c9081df0e49ffa86d15db4a6ba61530630e48707f987df42c9d3313537e81fc0", size = 2113098, upload-time = "2026-02-04T21:54:37.711Z" }, + { url = "https://files.pythonhosted.org/packages/b9/53/7278223c493ccfe4883239cf06c823c56460a8010e0fc778eef67858dc14/pywinpty-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:15e79d870e18b678fb8a5a6105fd38496b55697c66e6fc0378236026bc4d59e9", size = 234901, upload-time = "2026-02-04T21:53:31.35Z" }, + { url = "https://files.pythonhosted.org/packages/d7/48/57c3d4e63aa93280ad1b1c2b3a5412d9d1fdee9955fa0aef43a84518d31b/pywinpty-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:0f10e81d52d7f2c4d927f645f247028e64eaf205a3ed9e64dbd998122108a218", size = 2116037, upload-time = "2026-02-04T21:53:56.58Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, + { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4e/782eb6df91b6a9d9afa96c2dcfc5cac62562a68eb62a02210101f886014d/pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb", size = 1330426, upload-time = "2025-09-08T23:09:21.03Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ca/2b8693d06b1db4e0c084871e4c9d7842b561d0a6ff9d780640f5e3e9eb55/pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429", size = 906559, upload-time = "2025-09-08T23:09:22.983Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b3/b99b39e2cfdcebd512959780e4d299447fd7f46010b1d88d63324e2481ec/pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d", size = 863816, upload-time = "2025-09-08T23:09:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/61/b2/018fa8e8eefb34a625b1a45e2effcbc9885645b22cdd0a68283f758351e7/pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345", size = 666735, upload-time = "2025-09-08T23:09:26.297Z" }, + { url = "https://files.pythonhosted.org/packages/01/05/8ae778f7cd7c94030731ae2305e6a38f3a333b6825f56c0c03f2134ccf1b/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968", size = 1655425, upload-time = "2025-09-08T23:09:28.172Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ad/d69478a97a3f3142f9dbbbd9daa4fcf42541913a85567c36d4cfc19b2218/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098", size = 2033729, upload-time = "2025-09-08T23:09:30.097Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6d/e3c6ad05bc1cddd25094e66cc15ae8924e15c67e231e93ed2955c401007e/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f", size = 1891803, upload-time = "2025-09-08T23:09:31.875Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a7/97e8be0daaca157511563160b67a13d4fe76b195e3fa6873cb554ad46be3/pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78", size = 567627, upload-time = "2025-09-08T23:09:33.98Z" }, + { url = "https://files.pythonhosted.org/packages/5c/91/70bbf3a7c5b04c904261ef5ba224d8a76315f6c23454251bf5f55573a8a1/pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db", size = 632315, upload-time = "2025-09-08T23:09:36.097Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b5/a4173a83c7fd37f6bdb5a800ea338bc25603284e9ef8681377cec006ede4/pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc", size = 559833, upload-time = "2025-09-08T23:09:38.183Z" }, + { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, + { url = "https://files.pythonhosted.org/packages/57/f4/c2e978cf6b833708bad7d6396c3a20c19750585a1775af3ff13c435e1912/pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f", size = 836257, upload-time = "2025-09-08T23:10:07.635Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5f/4e10c7f57a4c92ab0fbb2396297aa8d618e6f5b9b8f8e9756d56f3e6fc52/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8", size = 800203, upload-time = "2025-09-08T23:10:09.436Z" }, + { url = "https://files.pythonhosted.org/packages/19/72/a74a007cd636f903448c6ab66628104b1fc5f2ba018733d5eabb94a0a6fb/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381", size = 758756, upload-time = "2025-09-08T23:10:11.733Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d4/30c25b91f2b4786026372f5ef454134d7f576fcf4ac58539ad7dd5de4762/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172", size = 567742, upload-time = "2025-09-08T23:10:14.732Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/ee86edad943438cd0316964020c4b6d09854414f9f945f8e289ea6fcc019/pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9", size = 544857, upload-time = "2025-09-08T23:10:16.431Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "attrs", marker = "python_full_version < '3.10'" }, + { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, + { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, + { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, + { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, + { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e7/0e1913dc52eee9c5cf8417c9813c4c55972a3f37d27cfa2e623b79b63dbc/regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5", size = 488185, upload-time = "2026-01-14T23:17:25.2Z" }, + { url = "https://files.pythonhosted.org/packages/78/df/c52c1ff4221529faad0953e197982fe9508c6dbb42327e31bf98ea07472a/regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815", size = 290628, upload-time = "2026-01-14T23:17:27.125Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/a2fef3717deaff647d7de2bccf899a576c7eaf042b6b271fc4474515fe97/regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b", size = 288509, upload-time = "2026-01-14T23:17:29.017Z" }, + { url = "https://files.pythonhosted.org/packages/70/89/faf5ee5c69168753c845a3d58b4683f61c899d162bfe1264fca88d5b3924/regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6", size = 781088, upload-time = "2026-01-14T23:17:30.961Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2c/707e5c380ad547c93686e21144e7e24dc2064dd84ec5b751b6dbdfc9be2b/regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b", size = 850516, upload-time = "2026-01-14T23:17:32.946Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3b/baa816cdcad1c0f8195f9f40ab2b2a2246c8a2989dcd90641c0c6559e3fd/regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df", size = 898124, upload-time = "2026-01-14T23:17:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/e7/74/1eb46bde30899825ed9fdf645eba16b7b97c49d12d300f5177989b9a09a4/regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e", size = 791290, upload-time = "2026-01-14T23:17:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5d/b72e176fb21e2ec248baed01151a342d1f44dd43c2b6bb6a41ad183b274e/regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c", size = 781996, upload-time = "2026-01-14T23:17:40.109Z" }, + { url = "https://files.pythonhosted.org/packages/61/0e/d3b3710eaafd994a4a71205d114abc38cda8691692a2ce2313abe68e7eb7/regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839", size = 767578, upload-time = "2026-01-14T23:17:42.134Z" }, + { url = "https://files.pythonhosted.org/packages/09/51/c6a6311833e040f95d229a34d82ac1cec2af8a5c00d58b244f2fceecef87/regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c", size = 774354, upload-time = "2026-01-14T23:17:44.392Z" }, + { url = "https://files.pythonhosted.org/packages/cc/97/c522d1f19fb2c549aaf680b115c110cd124c02062bc8c95f33db8583b4bb/regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077", size = 845297, upload-time = "2026-01-14T23:17:47.145Z" }, + { url = "https://files.pythonhosted.org/packages/99/a0/99468c386ab68a5e24c946c5c353c29c33a95523e275c17839f2446db15d/regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4", size = 755132, upload-time = "2026-01-14T23:17:49.796Z" }, + { url = "https://files.pythonhosted.org/packages/70/33/d5748c7b6c9d3621f12570583561ba529e2d1b12e4f70b8f17979b133e65/regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a", size = 835662, upload-time = "2026-01-14T23:17:52.559Z" }, + { url = "https://files.pythonhosted.org/packages/ad/15/1986972c276672505437f1ba3c9706c2d91f321cfb9b2f4d06e8bff1b999/regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3", size = 779513, upload-time = "2026-01-14T23:17:54.711Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f9/124f6a5cb3969d8e30471ed4f46cfc17c47aef1a9863ee8b4ba1d98b1bc4/regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a", size = 265923, upload-time = "2026-01-14T23:17:56.69Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c2/bb8fad7d27f1d71fc9772befd544bccd22eddc62a6735f57b003b4aff005/regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc", size = 277900, upload-time = "2026-01-14T23:17:58.72Z" }, + { url = "https://files.pythonhosted.org/packages/f7/fa/4e033327c1d8350bc812cac906d873984d3d4b39529252f392a47ccc356d/regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5", size = 270413, upload-time = "2026-01-14T23:18:00.764Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" }, + { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" }, + { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" }, + { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" }, + { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" }, + { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" }, + { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" }, + { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" }, + { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" }, + { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" }, + { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6c/252e83e1ce7583c81f26d1d884b2074d40a13977e1b6c9c50bbf9a7f1f5a/rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527", size = 372140, upload-time = "2025-08-27T12:15:05.441Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/949c195d927c5aeb0d0629d329a20de43a64c423a6aa53836290609ef7ec/rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d", size = 354086, upload-time = "2025-08-27T12:15:07.404Z" }, + { url = "https://files.pythonhosted.org/packages/9f/02/e43e332ad8ce4f6c4342d151a471a7f2900ed1d76901da62eb3762663a71/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8", size = 382117, upload-time = "2025-08-27T12:15:09.275Z" }, + { url = "https://files.pythonhosted.org/packages/d0/05/b0fdeb5b577197ad72812bbdfb72f9a08fa1e64539cc3940b1b781cd3596/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc", size = 394520, upload-time = "2025-08-27T12:15:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/67/1f/4cfef98b2349a7585181e99294fa2a13f0af06902048a5d70f431a66d0b9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1", size = 522657, upload-time = "2025-08-27T12:15:12.613Z" }, + { url = "https://files.pythonhosted.org/packages/44/55/ccf37ddc4c6dce7437b335088b5ca18da864b334890e2fe9aa6ddc3f79a9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125", size = 402967, upload-time = "2025-08-27T12:15:14.113Z" }, + { url = "https://files.pythonhosted.org/packages/74/e5/5903f92e41e293b07707d5bf00ef39a0eb2af7190aff4beaf581a6591510/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905", size = 384372, upload-time = "2025-08-27T12:15:15.842Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e3/fbb409e18aeefc01e49f5922ac63d2d914328430e295c12183ce56ebf76b/rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e", size = 401264, upload-time = "2025-08-27T12:15:17.388Z" }, + { url = "https://files.pythonhosted.org/packages/55/79/529ad07794e05cb0f38e2f965fc5bb20853d523976719400acecc447ec9d/rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e", size = 418691, upload-time = "2025-08-27T12:15:19.144Z" }, + { url = "https://files.pythonhosted.org/packages/33/39/6554a7fd6d9906fda2521c6d52f5d723dca123529fb719a5b5e074c15e01/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786", size = 558989, upload-time = "2025-08-27T12:15:21.087Z" }, + { url = "https://files.pythonhosted.org/packages/19/b2/76fa15173b6f9f445e5ef15120871b945fb8dd9044b6b8c7abe87e938416/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec", size = 589835, upload-time = "2025-08-27T12:15:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/5560a4b39bab780405bed8a88ee85b30178061d189558a86003548dea045/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b", size = 555227, upload-time = "2025-08-27T12:15:24.278Z" }, + { url = "https://files.pythonhosted.org/packages/52/d7/cd9c36215111aa65724c132bf709c6f35175973e90b32115dedc4ced09cb/rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52", size = 217899, upload-time = "2025-08-27T12:15:25.926Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e0/d75ab7b4dd8ba777f6b365adbdfc7614bbfe7c5f05703031dfa4b61c3d6c/rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab", size = 228725, upload-time = "2025-08-27T12:15:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" }, + { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" }, + { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" }, + { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" }, + { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" }, + { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" }, + { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ea/5463cd5048a7a2fcdae308b6e96432802132c141bfb9420260142632a0f1/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475", size = 371778, upload-time = "2025-08-27T12:16:13.851Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/f38c099db07f5114029c1467649d308543906933eebbc226d4527a5f4693/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f", size = 354394, upload-time = "2025-08-27T12:16:15.609Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/b76f97704d9dd8ddbd76fed4c4048153a847c5d6003afe20a6b5c3339065/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6", size = 382348, upload-time = "2025-08-27T12:16:17.251Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3f/ef23d3c1be1b837b648a3016d5bbe7cfe711422ad110b4081c0a90ef5a53/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3", size = 394159, upload-time = "2025-08-27T12:16:19.251Z" }, + { url = "https://files.pythonhosted.org/packages/74/8a/9e62693af1a34fd28b1a190d463d12407bd7cf561748cb4745845d9548d3/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3", size = 522775, upload-time = "2025-08-27T12:16:20.929Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/8d5bb122bf7a60976b54c5c99a739a3819f49f02d69df3ea2ca2aff47d5c/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8", size = 402633, upload-time = "2025-08-27T12:16:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/0f/0e/237948c1f425e23e0cf5a566d702652a6e55c6f8fbd332a1792eb7043daf/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400", size = 384867, upload-time = "2025-08-27T12:16:24.29Z" }, + { url = "https://files.pythonhosted.org/packages/d6/0a/da0813efcd998d260cbe876d97f55b0f469ada8ba9cbc47490a132554540/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485", size = 401791, upload-time = "2025-08-27T12:16:25.954Z" }, + { url = "https://files.pythonhosted.org/packages/51/78/c6c9e8a8aaca416a6f0d1b6b4a6ee35b88fe2c5401d02235d0a056eceed2/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1", size = 419525, upload-time = "2025-08-27T12:16:27.659Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/5af37e1d71487cf6d56dd1420dc7e0c2732c1b6ff612aa7a88374061c0a8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5", size = 559255, upload-time = "2025-08-27T12:16:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/40/7f/8b7b136069ef7ac3960eda25d832639bdb163018a34c960ed042dd1707c8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4", size = 590384, upload-time = "2025-08-27T12:16:31.005Z" }, + { url = "https://files.pythonhosted.org/packages/d8/06/c316d3f6ff03f43ccb0eba7de61376f8ec4ea850067dddfafe98274ae13c/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c", size = 555959, upload-time = "2025-08-27T12:16:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/60/94/384cf54c430b9dac742bbd2ec26c23feb78ded0d43d6d78563a281aec017/rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859", size = 228784, upload-time = "2025-08-27T12:16:34.428Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "ruff-api" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/4b/3959bc9feb67876d70cb252f4b832510d2604fb86e75888a904e3aa86c97/ruff_api-0.1.0.tar.gz", hash = "sha256:6d9a9ebc54b159bb37b6e6980e89493b8f34a4347f9e45c7a937420bc465ceac", size = 30963, upload-time = "2024-10-25T04:51:41.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/11/88eccc1b2c400482738d291b37ca6165c50306a49aed65841f497386ec03/ruff_api-0.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:607541be57a2e430ac9bd5bfbab39a2e33515947f2e40f3f1ca8aff6964d04b5", size = 4737011, upload-time = "2024-10-25T04:50:44.542Z" }, + { url = "https://files.pythonhosted.org/packages/3b/76/8d6795062b15aac71b3df8e31a51ab16f17e6c3b03dfcf95453d8d65d91f/ruff_api-0.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7e1ae962e331477e0c4b6c0ce7adc11081d911223ce1c25c48df231741c58699", size = 4534195, upload-time = "2024-10-25T04:50:46.611Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e5/9b28d315772034aa7a9058bbcf7f4099182f0a4d3c343c1e74c14e40f1d0/ruff_api-0.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec868721932df4fd1e7bcd90c5302e59001234a3e2c03ed4f0eaf938cf5b798", size = 5019248, upload-time = "2024-10-25T04:50:48.652Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f5/258896061aaba8e5f5d2feb03fd5f0f0b73fb369f3a8ab85da5a2ba3302e/ruff_api-0.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29c70028542b4008557e31bcfea9494da1ecf19e99656692b34ebcaae9d98f43", size = 5233762, upload-time = "2024-10-25T04:50:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/e6ef93c3606807b1207318c3f7726cc7905f6253e9abc34cea32c2e90d8b/ruff_api-0.1.0-cp310-none-win_amd64.whl", hash = "sha256:5b9e1f00cb46edbdbe6f96e685ac4c0f95e202fae6950066c936e34dbddf2290", size = 4481629, upload-time = "2024-10-25T04:50:52.837Z" }, + { url = "https://files.pythonhosted.org/packages/22/a0/e674c2153ddd6861b11cf54d9be8fb536d0acbcb6324d7376f7a356be80c/ruff_api-0.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5a336489eca057bf4c2ef170a6848a4d8f6cbba40c583fad2708f0fc2626af5d", size = 4737030, upload-time = "2024-10-25T04:50:54.738Z" }, + { url = "https://files.pythonhosted.org/packages/30/db/53036d387a9040244a270a475125293309b5250cce4dbca6d9634311e811/ruff_api-0.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6d122093ef95d58ebf9ad7ed287c63534eb9c3110f1ea10a1e8bf0cb75824bb", size = 4534484, upload-time = "2024-10-25T04:50:56.411Z" }, + { url = "https://files.pythonhosted.org/packages/17/ce/53c501b232a946a6d48b655b4e4ceb1aeeb09e5e020252f2d67b35bf327a/ruff_api-0.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:647bc419cc56aa4ddc11cd145c9048f9d97527b5dce4e31d06ffef6494a39c65", size = 5019460, upload-time = "2024-10-25T04:50:58.309Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/a545e2c08c65becb8f5f562933208c36998d78dbd403952aca5ddf32829e/ruff_api-0.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22137f4547ee0644b235b4ba9516a1cd549755fc22ce161c10abc516decefa9", size = 5233883, upload-time = "2024-10-25T04:51:00.267Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/11dbd2b4757061faaa6472f9ea49b888f89c03931d84527da594fdcff17b/ruff_api-0.1.0-cp311-none-win_amd64.whl", hash = "sha256:091fdd14bb256427b2eb72b719cf3df1dad58446ce08053238da1d3e0a64e8bd", size = 4481633, upload-time = "2024-10-25T04:51:02.105Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ef/dda4aba0c637ea4f108498a4b625fe4dde6e42586be17821bd900c43be44/ruff_api-0.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6a3826d8b88ccd9e3a307de204303d416549c01600e0d0a6d7e083f06ea7626a", size = 4734446, upload-time = "2024-10-25T04:51:04.302Z" }, + { url = "https://files.pythonhosted.org/packages/61/47/4b3ebbb24bed476136bca34ac51451ed9e5c90d36e96289193f353121ab7/ruff_api-0.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9eeb2183e37d14ef5c24c0413efcc4f8d646c647d42bcde6722a139f5d313ceb", size = 4532360, upload-time = "2024-10-25T04:51:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/47/91/e04c66fc6b02a0c2cea519f9d417bbf8feea67f1f438ffc2ce5c99fcb4ea/ruff_api-0.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a144747e3583cf476f1509f2733969fff3dbf73b08725aa4fdf7a385f3d2238b", size = 5019318, upload-time = "2024-10-25T04:51:07.929Z" }, + { url = "https://files.pythonhosted.org/packages/19/5d/fa705a70d1a338751df15a1a7b30efab8ac86e89207b4977ff065ae6c01b/ruff_api-0.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4412c2e9fccefe33ff5f8c99749995e9d56a324d2d107c805c8a1d9e5c9e229a", size = 5233200, upload-time = "2024-10-25T04:51:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/24/2a/22560db538635f98046e0d338ead76d66c5019250060eb8964156c9eef01/ruff_api-0.1.0-cp312-none-win_amd64.whl", hash = "sha256:647456e1d24adf2809b120fb299e2908a59da789462c30eefd917ac8251e7643", size = 4481760, upload-time = "2024-10-25T04:51:12.284Z" }, + { url = "https://files.pythonhosted.org/packages/b3/31/95419e6fe45c17e6239c7b5de7d69e55cc0c62e8caee8b6aa43ba8d3ab37/ruff_api-0.1.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e1d8947933df35503ee15ebee88dabc3d537f5a8bf9cccd9127fa14b4821ecfb", size = 4737000, upload-time = "2024-10-25T04:51:32.731Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e3/abdba21b9344423b7b56334b54e2a8c3f0191142e9facbd657bfcb375c60/ruff_api-0.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:def7d080c65f37233412b4c6188a90d7a91abc1dbcb171f82ebf56290a9349c5", size = 4534540, upload-time = "2024-10-25T04:51:34.343Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/848a044d44d0ae72092854ca338adb322080ed3a1cb2999262147c9da811/ruff_api-0.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42ff87c5273ec3a1797353b0f6bc6384dfbc8f67ce2a03a1bc11c6a5d5ea47c9", size = 5020144, upload-time = "2024-10-25T04:51:35.891Z" }, + { url = "https://files.pythonhosted.org/packages/68/ae/f0c53dd332f359974b9e3a4aa7ebfa2c8dfc2cf9fc8ab750da912bb28863/ruff_api-0.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1ef6a9ade72e83088de8e2c36918c1f8668a53fba521a4617d0f8b1b8531262", size = 5233958, upload-time = "2024-10-25T04:51:38.109Z" }, + { url = "https://files.pythonhosted.org/packages/e6/73/c93554b33c95e68db61b7ad39d6ea6d10560143a60f2e0154c53074243fc/ruff_api-0.1.0-cp39-none-win_amd64.whl", hash = "sha256:9c17ce23f995b07977c1b7a7720ccff6c4b8c273009c6cc0ffc6819692e44fb6", size = 4481808, upload-time = "2024-10-25T04:51:39.791Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, + { url = "https://files.pythonhosted.org/packages/94/60/13ccb63ea85bfe2e4fe6af602cf1272155f048906556d5ec8509da9dba42/safetensors-0.7.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b95a3fa7b3abb9b5b0e07668e808364d0d40f6bbbf9ae0faa8b5b210c97b140", size = 492627, upload-time = "2025-11-19T15:18:14.661Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2b/e2fde0d6334439908b0b0c4cba18b8ad76ea6a03b569d4a3388f423b4046/safetensors-0.7.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cfdead2f57330d76aa7234051dadfa7d4eedc0e5a27fd08e6f96714a92b00f09", size = 503861, upload-time = "2025-11-19T15:18:19.418Z" }, + { url = "https://files.pythonhosted.org/packages/f0/71/566e3dd559a9cef1b4775c239daae09e6b6a32ca8b45eb1db9a4dfa1ba81/safetensors-0.7.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc92bc2db7b45bda4510e4f51c59b00fe80b2d6be88928346e4294ce1c2abe7c", size = 623577, upload-time = "2025-11-19T15:18:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/3035c5c30c8a5a82c31c6b2ad6f8bcd45ea2ddd9a8088840406bcf997413/safetensors-0.7.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6999421eb8ba9df4450a16d9184fcb7bef26240b9f98e95401f17af6c2210b71", size = 532524, upload-time = "2025-11-19T15:18:29.334Z" }, +] + +[[package]] +name = "sam3" +source = { editable = "." } +dependencies = [ + { name = "einops" }, + { name = "ftfy" }, + { name = "huggingface-hub" }, + { name = "iopath" }, + { name = "numpy" }, + { name = "psutil" }, + { name = "regex" }, + { name = "timm" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "gitpython" }, + { name = "numba", version = "0.60.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numba", version = "0.63.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "opencv-python" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pycocotools" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-cov" }, + { name = "python-rapidjson" }, + { name = "ruff-api" }, + { name = "ufmt" }, + { name = "usort" }, + { name = "yt-dlp", version = "2025.10.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "yt-dlp", version = "2026.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +notebooks = [ + { name = "decord" }, + { name = "einops" }, + { name = "ipycanvas" }, + { name = "ipympl" }, + { name = "ipywidgets" }, + { name = "jupyter" }, + { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "notebook" }, + { name = "opencv-python" }, + { name = "pycocotools" }, + { name = "scikit-image", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scikit-learn", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +train = [ + { name = "fairscale" }, + { name = "fvcore" }, + { name = "hydra-core" }, + { name = "scikit-image", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scikit-learn", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "submitit" }, + { name = "tensorboard" }, + { name = "torchmetrics" }, + { name = "zstandard" }, +] + +[package.dev-dependencies] +dev = [ + { name = "black" }, + { name = "gitpython" }, + { name = "numba", version = "0.60.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numba", version = "0.63.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "opencv-python" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pycocotools" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-cov" }, + { name = "python-rapidjson" }, + { name = "ruff-api" }, + { name = "ufmt" }, + { name = "usort" }, + { name = "yt-dlp", version = "2025.10.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "yt-dlp", version = "2026.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = "==24.2.0" }, + { name = "decord", marker = "extra == 'notebooks'" }, + { name = "einops" }, + { name = "einops", marker = "extra == 'notebooks'" }, + { name = "fairscale", marker = "extra == 'train'" }, + { name = "ftfy", specifier = "==6.1.1" }, + { name = "fvcore", marker = "extra == 'train'" }, + { name = "gitpython", marker = "extra == 'dev'", specifier = "==3.1.31" }, + { name = "huggingface-hub" }, + { name = "hydra-core", marker = "extra == 'train'" }, + { name = "iopath", specifier = ">=0.1.10" }, + { name = "ipycanvas", marker = "extra == 'notebooks'" }, + { name = "ipympl", marker = "extra == 'notebooks'" }, + { name = "ipywidgets", marker = "extra == 'notebooks'" }, + { name = "jupyter", marker = "extra == 'notebooks'" }, + { name = "matplotlib", marker = "extra == 'notebooks'" }, + { name = "notebook", marker = "extra == 'notebooks'" }, + { name = "numba", marker = "extra == 'dev'" }, + { name = "numpy", specifier = ">=1.26,<2" }, + { name = "opencv-python", marker = "extra == 'dev'" }, + { name = "opencv-python", marker = "extra == 'notebooks'" }, + { name = "pandas", marker = "extra == 'dev'" }, + { name = "psutil" }, + { name = "pycocotools", marker = "extra == 'dev'" }, + { name = "pycocotools", marker = "extra == 'notebooks'" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-cov", marker = "extra == 'dev'" }, + { name = "python-rapidjson", marker = "extra == 'dev'" }, + { name = "regex" }, + { name = "ruff-api", marker = "extra == 'dev'", specifier = "==0.1.0" }, + { name = "scikit-image", marker = "extra == 'notebooks'" }, + { name = "scikit-image", marker = "extra == 'train'" }, + { name = "scikit-learn", marker = "extra == 'notebooks'" }, + { name = "scikit-learn", marker = "extra == 'train'" }, + { name = "scipy", marker = "extra == 'train'" }, + { name = "submitit", marker = "extra == 'train'" }, + { name = "tensorboard", marker = "extra == 'train'" }, + { name = "timm", specifier = ">=1.0.17" }, + { name = "torchmetrics", marker = "extra == 'train'" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "ufmt", marker = "extra == 'dev'", specifier = "==2.8.0" }, + { name = "usort", marker = "extra == 'dev'", specifier = "==1.0.2" }, + { name = "yt-dlp", marker = "extra == 'dev'" }, + { name = "zstandard", marker = "extra == 'train'" }, +] +provides-extras = ["dev", "notebooks", "train"] + +[package.metadata.requires-dev] +dev = [ + { name = "black", specifier = "==24.2.0" }, + { name = "gitpython", specifier = "==3.1.31" }, + { name = "numba" }, + { name = "opencv-python" }, + { name = "pandas" }, + { name = "pycocotools" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "python-rapidjson" }, + { name = "ruff-api", specifier = "==0.1.0" }, + { name = "ufmt", specifier = "==2.8.0" }, + { name = "usort", specifier = "==1.0.2" }, + { name = "yt-dlp" }, +] + +[[package]] +name = "scikit-image" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "imageio", marker = "python_full_version < '3.10'" }, + { name = "lazy-loader", marker = "python_full_version < '3.10'" }, + { name = "networkx", version = "3.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "tifffile", version = "2024.8.30", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/c5/bcd66bf5aae5587d3b4b69c74bee30889c46c9778e858942ce93a030e1f3/scikit_image-0.24.0.tar.gz", hash = "sha256:5d16efe95da8edbeb363e0c4157b99becbd650a60b77f6e3af5768b66cf007ab", size = 22693928, upload-time = "2024-06-18T19:05:31.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/82/d4eaa6e441f28a783762093a3c74bcc4a67f1c65bf011414ad4ea85187d8/scikit_image-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb3bc0264b6ab30b43c4179ee6156bc18b4861e78bb329dd8d16537b7bbf827a", size = 14051470, upload-time = "2024-06-18T19:03:37.385Z" }, + { url = "https://files.pythonhosted.org/packages/65/15/1879307aaa2c771aa8ef8f00a171a85033bffc6b2553cfd2657426881452/scikit_image-0.24.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:9c7a52e20cdd760738da38564ba1fed7942b623c0317489af1a598a8dedf088b", size = 13385822, upload-time = "2024-06-18T19:03:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b8/2d52864714b82122f4a36f47933f61f1cd2a6df34987873837f8064d4fdf/scikit_image-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93f46e6ce42e5409f4d09ce1b0c7f80dd7e4373bcec635b6348b63e3c886eac8", size = 14216787, upload-time = "2024-06-18T19:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/40/2e/8b39cd2c347490dbe10adf21fd50bbddb1dada5bb0512c3a39371285eb62/scikit_image-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39ee0af13435c57351a3397eb379e72164ff85161923eec0c38849fecf1b4764", size = 14866533, upload-time = "2024-06-18T19:03:56.286Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/3fcd68d034db5d29c974e964d03deec9d0fbf9410ff0a0b95efff70947f6/scikit_image-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ac7913b028b8aa780ffae85922894a69e33d1c0bf270ea1774f382fe8bf95e7", size = 12864601, upload-time = "2024-06-18T19:04:00.868Z" }, + { url = "https://files.pythonhosted.org/packages/90/e3/564beb0c78bf83018a146dfcdc959c99c10a0d136480b932a350c852adbc/scikit_image-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:272909e02a59cea3ed4aa03739bb88df2625daa809f633f40b5053cf09241831", size = 14020429, upload-time = "2024-06-18T19:04:07.18Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f6/be8b16d8ab6ebf19057877c2aec905cbd438dd92ca64b8efe9e9af008fa3/scikit_image-0.24.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:190ebde80b4470fe8838764b9b15f232a964f1a20391663e31008d76f0c696f7", size = 13371950, upload-time = "2024-06-18T19:04:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2e/3a949995f8fc2a65b15a4964373e26c5601cb2ea68f36b115571663e7a38/scikit_image-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c98cc695005faf2b79904e4663796c977af22586ddf1b12d6af2fa22842dc2", size = 14197889, upload-time = "2024-06-18T19:04:17.181Z" }, + { url = "https://files.pythonhosted.org/packages/ad/96/138484302b8ec9a69cdf65e8d4ab47a640a3b1a8ea3c437e1da3e1a5a6b8/scikit_image-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa27b3a0dbad807b966b8db2d78da734cb812ca4787f7fbb143764800ce2fa9c", size = 14861425, upload-time = "2024-06-18T19:04:27.363Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/d5e97115733e2dc657e99868ae0237705b79d0c81f6ced21b8f0799a30d1/scikit_image-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:dacf591ac0c272a111181afad4b788a27fe70d213cfddd631d151cbc34f8ca2c", size = 12843506, upload-time = "2024-06-18T19:04:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/45ad3b8b8ab8d275a48a9d1016c4beb1c2801a7a13e384268861d01145c1/scikit_image-0.24.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6fccceb54c9574590abcddc8caf6cefa57c13b5b8b4260ab3ff88ad8f3c252b3", size = 14101823, upload-time = "2024-06-18T19:04:39.576Z" }, + { url = "https://files.pythonhosted.org/packages/6e/75/db10ee1bc7936b411d285809b5fe62224bbb1b324a03dd703582132ce5ee/scikit_image-0.24.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ccc01e4760d655aab7601c1ba7aa4ddd8b46f494ac46ec9c268df6f33ccddf4c", size = 13420758, upload-time = "2024-06-18T19:04:45.645Z" }, + { url = "https://files.pythonhosted.org/packages/87/fd/07a7396962abfe22a285a922a63d18e4d5ec48eb5dbb1c06e96fb8fb6528/scikit_image-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18836a18d3a7b6aca5376a2d805f0045826bc6c9fc85331659c33b4813e0b563", size = 14256813, upload-time = "2024-06-18T19:04:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/2c/24/4bcd94046b409ac4d63e2f92e46481f95f5006a43e68f6ab2b24f5d70ab4/scikit_image-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8579bda9c3f78cb3b3ed8b9425213c53a25fa7e994b7ac01f2440b395babf660", size = 15013039, upload-time = "2024-06-18T19:04:56.433Z" }, + { url = "https://files.pythonhosted.org/packages/d9/17/b561823143eb931de0f82fed03ae128ef954a9641309602ea0901c357f95/scikit_image-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:82ab903afa60b2da1da2e6f0c8c65e7c8868c60a869464c41971da929b3e82bc", size = 12949363, upload-time = "2024-06-18T19:05:02.773Z" }, + { url = "https://files.pythonhosted.org/packages/93/8e/b6e50d8a6572daf12e27acbf9a1722fdb5e6bfc64f04a5fefa2a71fea0c3/scikit_image-0.24.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef04360eda372ee5cd60aebe9be91258639c86ae2ea24093fb9182118008d009", size = 14083010, upload-time = "2024-06-18T19:05:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6c/f528c6b80b4e9d38444d89f0d1160797d20c640b7a8cabd8b614ac600b79/scikit_image-0.24.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e9aadb442360a7e76f0c5c9d105f79a83d6df0e01e431bd1d5757e2c5871a1f3", size = 13414235, upload-time = "2024-06-18T19:05:11.58Z" }, + { url = "https://files.pythonhosted.org/packages/52/03/59c52aa59b952aafcf19163e5d7e924e6156c3d9e9c86ea3372ad31d90f8/scikit_image-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e37de6f4c1abcf794e13c258dc9b7d385d5be868441de11c180363824192ff7", size = 14238540, upload-time = "2024-06-18T19:05:17.481Z" }, + { url = "https://files.pythonhosted.org/packages/f0/cc/1a58efefb9b17c60d15626b33416728003028d5d51f0521482151a222560/scikit_image-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4688c18bd7ec33c08d7bf0fd19549be246d90d5f2c1d795a89986629af0a1e83", size = 14883801, upload-time = "2024-06-18T19:05:23.231Z" }, + { url = "https://files.pythonhosted.org/packages/9d/63/233300aa76c65a442a301f9d2416a9b06c91631287bd6dd3d6b620040096/scikit_image-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:56dab751d20b25d5d3985e95c9b4e975f55573554bd76b0aedf5875217c93e69", size = 12891952, upload-time = "2024-06-18T19:05:27.173Z" }, +] + +[[package]] +name = "scikit-image" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "imageio", marker = "python_full_version == '3.10.*'" }, + { name = "lazy-loader", marker = "python_full_version == '3.10.*'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", marker = "python_full_version == '3.10.*'" }, + { name = "packaging", marker = "python_full_version == '3.10.*'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/cb/016c63f16065c2d333c8ed0337e18a5cdf9bc32d402e4f26b0db362eb0e2/scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78", size = 13988922, upload-time = "2025-02-18T18:04:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/30/ca/ff4731289cbed63c94a0c9a5b672976603118de78ed21910d9060c82e859/scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063", size = 13192698, upload-time = "2025-02-18T18:04:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/39/6d/a2aadb1be6d8e149199bb9b540ccde9e9622826e1ab42fe01de4c35ab918/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99", size = 14153634, upload-time = "2025-02-18T18:04:18.496Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/916e7d9ee4721031b2f625db54b11d8379bd51707afaa3e5a29aecf10bc4/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09", size = 14767545, upload-time = "2025-02-18T18:04:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ee/c53a009e3997dda9d285402f19226fbd17b5b3cb215da391c4ed084a1424/scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054", size = 12812908, upload-time = "2025-02-18T18:04:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, + { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, + { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, +] + +[[package]] +name = "scikit-image" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "imageio", marker = "python_full_version >= '3.11'" }, + { name = "lazy-loader", 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 = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tifffile", version = "2026.1.28", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/16/8a407688b607f86f81f8c649bf0d68a2a6d67375f18c2d660aba20f5b648/scikit_image-0.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b1ede33a0fb3731457eaf53af6361e73dd510f449dac437ab54573b26788baf0", size = 12355510, upload-time = "2025-12-20T17:10:31.628Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f9/7efc088ececb6f6868fd4475e16cfafc11f242ce9ab5fc3557d78b5da0d4/scikit_image-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7af7aa331c6846bd03fa28b164c18d0c3fd419dbb888fb05e958ac4257a78fdd", size = 12056334, upload-time = "2025-12-20T17:10:34.559Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/bc7fb91fb5ff65ef42346c8b7ee8b09b04eabf89235ab7dbfdfd96cbd1ea/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea6207d9e9d21c3f464efe733121c0504e494dbdc7728649ff3e23c3c5a4953", size = 13297768, upload-time = "2025-12-20T17:10:37.733Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2a/e71c1a7d90e70da67b88ccc609bd6ae54798d5847369b15d3a8052232f9d/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74aa5518ccea28121f57a95374581d3b979839adc25bb03f289b1bc9b99c58af", size = 13711217, upload-time = "2025-12-20T17:10:40.935Z" }, + { url = "https://files.pythonhosted.org/packages/d4/59/9637ee12c23726266b91296791465218973ce1ad3e4c56fc81e4d8e7d6e1/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c244656de905e195a904e36dbc18585e06ecf67d90f0482cbde63d7f9ad59d", size = 14337782, upload-time = "2025-12-20T17:10:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5c/a3e1e0860f9294663f540c117e4bf83d55e5b47c281d475cc06227e88411/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21a818ee6ca2f2131b9e04d8eb7637b5c18773ebe7b399ad23dcc5afaa226d2d", size = 14805997, upload-time = "2025-12-20T17:10:45.93Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c6/2eeacf173da041a9e388975f54e5c49df750757fcfc3ee293cdbbae1ea0a/scikit_image-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:9490360c8d3f9a7e85c8de87daf7c0c66507960cf4947bb9610d1751928721c7", size = 11878486, upload-time = "2025-12-20T17:10:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a4/a852c4949b9058d585e762a66bf7e9a2cd3be4795cd940413dfbfbb0ce79/scikit_image-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:0baa0108d2d027f34d748e84e592b78acc23e965a5de0e4bb03cf371de5c0581", size = 11346518, upload-time = "2025-12-20T17:10:50.575Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/e13757982264b33a1621628f86b587e9a73a13f5256dad49b19ba7dc9083/scikit_image-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c", size = 12376452, upload-time = "2025-12-20T17:10:52.796Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/f8dd17d0510f9911f9f17ba301f7455328bf13dae416560126d428de9568/scikit_image-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37", size = 12061567, upload-time = "2025-12-20T17:10:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/c70120a6880579fb42b91567ad79feb4772f7be72e8d52fec403a3dde0c6/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44", size = 13084214, upload-time = "2025-12-20T17:10:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a2/70401a107d6d7466d64b466927e6b96fcefa99d57494b972608e2f8be50f/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466", size = 13561683, upload-time = "2025-12-20T17:10:59.49Z" }, + { url = "https://files.pythonhosted.org/packages/13/a5/48bdfd92794c5002d664e0910a349d0a1504671ef5ad358150f21643c79a/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932", size = 14112147, upload-time = "2025-12-20T17:11:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b5/ac71694da92f5def5953ca99f18a10fe98eac2dd0a34079389b70b4d0394/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70", size = 14661625, upload-time = "2025-12-20T17:11:04.528Z" }, + { url = "https://files.pythonhosted.org/packages/23/4d/a3cc1e96f080e253dad2251bfae7587cf2b7912bcd76fd43fd366ff35a87/scikit_image-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0", size = 11911059, upload-time = "2025-12-20T17:11:06.61Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/d1b8055f584acc937478abf4550d122936f420352422a1a625eef2c605d8/scikit_image-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8", size = 11348740, upload-time = "2025-12-20T17:11:09.118Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "joblib", marker = "python_full_version < '3.10'" }, + { name = "numpy", marker = "python_full_version < '3.10'" }, + { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312, upload-time = "2025-01-10T08:07:55.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/3a/f4597eb41049110b21ebcbb0bcb43e4035017545daa5eedcfeb45c08b9c5/scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e", size = 12067702, upload-time = "2025-01-10T08:05:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/0423e5e1fd1c6ec5be2352ba05a537a473c1677f8188b9306097d684b327/scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36", size = 11112765, upload-time = "2025-01-10T08:06:00.272Z" }, + { url = "https://files.pythonhosted.org/packages/70/95/d5cb2297a835b0f5fc9a77042b0a2d029866379091ab8b3f52cc62277808/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5", size = 12643991, upload-time = "2025-01-10T08:06:04.813Z" }, + { url = "https://files.pythonhosted.org/packages/b7/91/ab3c697188f224d658969f678be86b0968ccc52774c8ab4a86a07be13c25/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b", size = 13497182, upload-time = "2025-01-10T08:06:08.42Z" }, + { url = "https://files.pythonhosted.org/packages/17/04/d5d556b6c88886c092cc989433b2bab62488e0f0dafe616a1d5c9cb0efb1/scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002", size = 11125517, upload-time = "2025-01-10T08:06:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/e291c29670795406a824567d1dfc91db7b699799a002fdaa452bceea8f6e/scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33", size = 12102620, upload-time = "2025-01-10T08:06:16.675Z" }, + { url = "https://files.pythonhosted.org/packages/25/92/ee1d7a00bb6b8c55755d4984fd82608603a3cc59959245068ce32e7fb808/scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d", size = 11116234, upload-time = "2025-01-10T08:06:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/30/cd/ed4399485ef364bb25f388ab438e3724e60dc218c547a407b6e90ccccaef/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2", size = 12592155, upload-time = "2025-01-10T08:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8", size = 13497069, upload-time = "2025-01-10T08:06:32.515Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/c5b78606743a1f28eae8f11973de6613a5ee87366796583fb74c67d54939/scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415", size = 11139809, upload-time = "2025-01-10T08:06:35.514Z" }, + { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516, upload-time = "2025-01-10T08:06:40.009Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837, upload-time = "2025-01-10T08:06:43.305Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728, upload-time = "2025-01-10T08:06:47.618Z" }, + { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700, upload-time = "2025-01-10T08:06:50.888Z" }, + { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613, upload-time = "2025-01-10T08:06:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/d2/37/b305b759cc65829fe1b8853ff3e308b12cdd9d8884aa27840835560f2b42/scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1", size = 12101868, upload-time = "2025-01-10T08:07:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/83/74/f64379a4ed5879d9db744fe37cfe1978c07c66684d2439c3060d19a536d8/scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e", size = 11144062, upload-time = "2025-01-10T08:07:37.67Z" }, + { url = "https://files.pythonhosted.org/packages/fd/dc/d5457e03dc9c971ce2b0d750e33148dd060fefb8b7dc71acd6054e4bb51b/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107", size = 12693173, upload-time = "2025-01-10T08:07:42.713Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/b1d2188967c3204c78fa79c9263668cf1b98060e8e58d1a730fe5b2317bb/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422", size = 13518605, upload-time = "2025-01-10T08:07:46.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d8/8d603bdd26601f4b07e2363032b8565ab82eb857f93d86d0f7956fcf4523/scikit_learn-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b", size = 11155078, upload-time = "2025-01-10T08:07:51.376Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "joblib", marker = "python_full_version == '3.10.*'" }, + { name = "numpy", marker = "python_full_version == '3.10.*'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "threadpoolctl", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, +] + +[[package]] +name = "scipy" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720, upload-time = "2024-05-23T03:29:26.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076, upload-time = "2024-05-23T03:19:01.687Z" }, + { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232, upload-time = "2024-05-23T03:19:09.089Z" }, + { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202, upload-time = "2024-05-23T03:19:15.138Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335, upload-time = "2024-05-23T03:19:21.984Z" }, + { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728, upload-time = "2024-05-23T03:19:28.225Z" }, + { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588, upload-time = "2024-05-23T03:19:35.661Z" }, + { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805, upload-time = "2024-05-23T03:19:43.081Z" }, + { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687, upload-time = "2024-05-23T03:19:48.799Z" }, + { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638, upload-time = "2024-05-23T03:19:55.104Z" }, + { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931, upload-time = "2024-05-23T03:20:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145, upload-time = "2024-05-23T03:20:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227, upload-time = "2024-05-23T03:20:16.433Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301, upload-time = "2024-05-23T03:20:23.538Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348, upload-time = "2024-05-23T03:20:29.885Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062, upload-time = "2024-05-23T03:20:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311, upload-time = "2024-05-23T03:20:42.086Z" }, + { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493, upload-time = "2024-05-23T03:20:48.292Z" }, + { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955, upload-time = "2024-05-23T03:20:55.091Z" }, + { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927, upload-time = "2024-05-23T03:21:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538, upload-time = "2024-05-23T03:21:07.634Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190, upload-time = "2024-05-23T03:21:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244, upload-time = "2024-05-23T03:21:21.827Z" }, + { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637, upload-time = "2024-05-23T03:21:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440, upload-time = "2024-05-23T03:21:35.888Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, + { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, + { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, + { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, + { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/08629657ac6c0da198487ce8cd3de78e02cfde42b7f34117d56a3fe249dc/scipy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:255c0da161bd7b32a6c898e7891509e8a9289f0b1c6c7d96142ee0d2b114c2ea", size = 36380284, upload-time = "2026-01-10T21:25:15.632Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, +] + +[[package]] +name = "send2trash" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "stdlibs" +version = "2025.10.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/83/ac15c4a3c059725dcb5f5d76270b986808cc12d2d7d417ee540d37609e46/stdlibs-2025.10.28.tar.gz", hash = "sha256:18db81f45f7783ddf86b80771e061782c70e2f4a8642843b3c80b42cd774b24f", size = 20108, upload-time = "2025-10-28T22:14:42.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/de/5fcc806280950b9535d3892c7f1f3477efc4c2f8624ae6c0b2c3baf9a339/stdlibs-2025.10.28-py3-none-any.whl", hash = "sha256:fc25a3608c417c7fecec06736a2671adaceafc9f20c3f536d967e894a998afea", size = 59232, upload-time = "2025-10-28T22:14:40.799Z" }, +] + +[[package]] +name = "submitit" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/86/497018fb3b74e71bef45df82762b176e6b3d159f29941c20d2f141ec4096/submitit-1.5.4.tar.gz", hash = "sha256:7100848bd1cdda79c7196e54ee830793ae75fd7adde0c5bef738d72360a07508", size = 81538, upload-time = "2025-12-17T19:20:03.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/bb/711e1c2ebd18a21202c972dd5d5c8e09a921f2d3560e3a53d6350c808ab7/submitit-1.5.4-py3-none-any.whl", hash = "sha256:c26f3a7c8d4150eaf70b1da71e2023e9e9936c93e8342ed7db910f29158561c5", size = 76043, upload-time = "2025-12-17T19:20:01.941Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +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" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "tensorboard" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "absl-py", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "grpcio" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "protobuf" }, + { name = "setuptools" }, + { name = "tensorboard-data-server" }, + { name = "werkzeug" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680, upload-time = "2025-07-17T19:20:49.638Z" }, +] + +[[package]] +name = "tensorboard-data-server" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356, upload-time = "2023-10-23T21:23:32.16Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598, upload-time = "2023-10-23T21:23:33.714Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" }, +] + +[[package]] +name = "termcolor" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "(python_full_version < '3.10' and os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tifffile" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/30/7017e5560154c100cad3a801c02adb48879cd8e8cb862b82696d84187184/tifffile-2024.8.30.tar.gz", hash = "sha256:2c9508fe768962e30f87def61819183fb07692c258cb175b3c114828368485a4", size = 365714, upload-time = "2024-08-31T17:32:43.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/4f/73714b1c1d339b1545cac28764e39f88c69468b5e10e51f327f9aa9d55b9/tifffile-2024.8.30-py3-none-any.whl", hash = "sha256:8bc59a8f02a2665cd50a910ec64961c5373bee0b8850ec89d3b7b485bf7be7ad", size = 227262, upload-time = "2024-08-31T17:32:41.87Z" }, +] + +[[package]] +name = "tifffile" +version = "2025.5.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/06/bd0a6097da704a7a7c34a94cfd771c3ea3c2f405dd214e790d22c93f6be1/tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad", size = 226533, upload-time = "2025-05-10T19:22:27.279Z" }, +] + +[[package]] +name = "tifffile" +version = "2026.1.28" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/32/38498d2a1a5d70f33f6c3909bbad48557c9a54b0e33a9307ff06b6d416ba/tifffile-2026.1.28.tar.gz", hash = "sha256:537ae6466a8bb555c336108bb1878d8319d52c9c738041d3349454dea6956e1c", size = 374675, upload-time = "2026-01-29T05:17:24.992Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/19/529b28ca338c5a88315e71e672badc85eef89460c248c4164f6ce058f8c7/tifffile-2026.1.28-py3-none-any.whl", hash = "sha256:45b08a19cf603dd99952eff54a61519626a1912e4e2a4d355f05938fe4a6e9fd", size = 233011, upload-time = "2026-01-29T05:17:23.078Z" }, +] + +[[package]] +name = "timm" +version = "1.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "torchvision", version = "0.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/9d/0ea45640be447445c8664ce2b10c74f763b0b0b9ed11620d41a4d4baa10c/timm-1.0.24.tar.gz", hash = "sha256:c7b909f43fe2ef8fe62c505e270cd4f1af230dfbc37f2ee93e3608492b9d9a40", size = 2412239, upload-time = "2026-01-07T00:26:17.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/dd/c1f5b0890f7b5db661bde0864b41cb0275be76851047e5f7e085fe0b455a/timm-1.0.24-py3-none-any.whl", hash = "sha256:8301ac783410c6ad72c73c49326af6d71a9e4d1558238552796e825c2464913f", size = 2560563, upload-time = "2026-01-07T00:26:13.956Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/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 = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "torch" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, + { name = "networkx", version = "3.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "nvidia-cublas-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", version = "2.27.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "sympy", marker = "python_full_version < '3.10'" }, + { name = "triton", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/28/110f7274254f1b8476c561dada127173f994afa2b1ffc044efb773c15650/torch-2.8.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:0be92c08b44009d4131d1ff7a8060d10bafdb7ddcb7359ef8d8c5169007ea905", size = 102052793, upload-time = "2025-08-06T14:53:15.852Z" }, + { url = "https://files.pythonhosted.org/packages/70/1c/58da560016f81c339ae14ab16c98153d51c941544ae568da3cb5b1ceb572/torch-2.8.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:89aa9ee820bb39d4d72b794345cccef106b574508dd17dbec457949678c76011", size = 888025420, upload-time = "2025-08-06T14:54:18.014Z" }, + { url = "https://files.pythonhosted.org/packages/70/87/f69752d0dd4ba8218c390f0438130c166fa264a33b7025adb5014b92192c/torch-2.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e8e5bf982e87e2b59d932769938b698858c64cc53753894be25629bdf5cf2f46", size = 241363614, upload-time = "2025-08-06T14:53:31.496Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d6/e6d4c57e61c2b2175d3aafbfb779926a2cfd7c32eeda7c543925dceec923/torch-2.8.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a3f16a58a9a800f589b26d47ee15aca3acf065546137fc2af039876135f4c760", size = 73611154, upload-time = "2025-08-06T14:53:10.919Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c4/3e7a3887eba14e815e614db70b3b529112d1513d9dae6f4d43e373360b7f/torch-2.8.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:220a06fd7af8b653c35d359dfe1aaf32f65aa85befa342629f716acb134b9710", size = 102073391, upload-time = "2025-08-06T14:53:20.937Z" }, + { url = "https://files.pythonhosted.org/packages/5a/63/4fdc45a0304536e75a5e1b1bbfb1b56dd0e2743c48ee83ca729f7ce44162/torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c12fa219f51a933d5f80eeb3a7a5d0cbe9168c0a14bbb4055f1979431660879b", size = 888063640, upload-time = "2025-08-06T14:55:05.325Z" }, + { url = "https://files.pythonhosted.org/packages/84/57/2f64161769610cf6b1c5ed782bd8a780e18a3c9d48931319f2887fa9d0b1/torch-2.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c7ef765e27551b2fbfc0f41bcf270e1292d9bf79f8e0724848b1682be6e80aa", size = 241366752, upload-time = "2025-08-06T14:53:38.692Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5e/05a5c46085d9b97e928f3f037081d3d2b87fb4b4195030fc099aaec5effc/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:5ae0524688fb6707c57a530c2325e13bb0090b745ba7b4a2cd6a3ce262572916", size = 73621174, upload-time = "2025-08-06T14:53:25.44Z" }, + { url = "https://files.pythonhosted.org/packages/49/0c/2fd4df0d83a495bb5e54dca4474c4ec5f9c62db185421563deeb5dabf609/torch-2.8.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e2fab4153768d433f8ed9279c8133a114a034a61e77a3a104dcdf54388838705", size = 101906089, upload-time = "2025-08-06T14:53:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/99/a8/6acf48d48838fb8fe480597d98a0668c2beb02ee4755cc136de92a0a956f/torch-2.8.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2aca0939fb7e4d842561febbd4ffda67a8e958ff725c1c27e244e85e982173c", size = 887913624, upload-time = "2025-08-06T14:56:44.33Z" }, + { url = "https://files.pythonhosted.org/packages/af/8a/5c87f08e3abd825c7dfecef5a0f1d9aa5df5dd0e3fd1fa2f490a8e512402/torch-2.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:2f4ac52f0130275d7517b03a33d2493bab3693c83dcfadf4f81688ea82147d2e", size = 241326087, upload-time = "2025-08-06T14:53:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/be/66/5c9a321b325aaecb92d4d1855421e3a055abd77903b7dab6575ca07796db/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:619c2869db3ada2c0105487ba21b5008defcc472d23f8b80ed91ac4a380283b0", size = 73630478, upload-time = "2025-08-06T14:53:57.144Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b0/a321f27270049baa12f5c3fb0d6ceea005634787e3af9a8d75dce8306b0a/torch-2.8.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:da6afa31c13b669d4ba49d8a2169f0db2c3ec6bec4af898aa714f401d4c38904", size = 102059214, upload-time = "2025-08-06T14:55:33.433Z" }, + { url = "https://files.pythonhosted.org/packages/fd/dd/1630cb51b10d3d2e97db95e5a84c32def81fc26b005bce6fc880b0e6db81/torch-2.8.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:06fcee8000e5c62a9f3e52a688b9c5abb7c6228d0e56e3452983416025c41381", size = 888024302, upload-time = "2025-08-06T14:57:28.23Z" }, + { url = "https://files.pythonhosted.org/packages/b9/dc/1f1f621afe15e3c496e1e8f94f8903f75f87e7d642d5a985e92210cc208d/torch-2.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:5128fe752a355d9308e56af1ad28b15266fe2da5948660fad44de9e3a9e36e8c", size = 241249338, upload-time = "2025-08-06T14:57:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/ae/95/ae26263aceb3d57b821179f827d0e321373ed49423e603dd5906ab14a730/torch-2.8.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:e9f071f5b52a9f6970dc8a919694b27a91ae9dc08898b2b988abbef5eddfd1ae", size = 73610795, upload-time = "2025-08-06T14:57:11.513Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "cuda-bindings", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "fsspec", version = "2026.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jinja2", marker = "python_full_version >= '3.10'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cublas-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", version = "2.27.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy", marker = "python_full_version >= '3.10'" }, + { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +wheels = [ + { 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" }, +] + +[[package]] +name = "torchmetrics" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lightning-utilities" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/48a887a59ecc4a10ce9e8b35b3e3c5cef29d902c4eac143378526e7485cb/torchmetrics-1.8.2.tar.gz", hash = "sha256:cf64a901036bf107f17a524009eea7781c9c5315d130713aeca5747a686fe7a5", size = 580679, upload-time = "2025-09-03T14:00:54.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/21/aa0f434434c48490f91b65962b1ce863fdcce63febc166ca9fe9d706c2b6/torchmetrics-1.8.2-py3-none-any.whl", hash = "sha256:08382fd96b923e39e904c4d570f3d49e2cc71ccabd2a94e0f895d1f0dac86242", size = 983161, upload-time = "2025-09-03T14:00:51.921Z" }, +] + +[[package]] +name = "torchvision" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.10'" }, + { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/49/5ad5c3ff4920be0adee9eb4339b4fb3b023a0fc55b9ed8dbc73df92946b8/torchvision-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7266871daca00ad46d1c073e55d972179d12a58fa5c9adec9a3db9bbed71284a", size = 1856885, upload-time = "2025-08-06T14:57:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/25/44/ddd56d1637bac42a8c5da2c8c440d8a28c431f996dd9790f32dd9a96ca6e/torchvision-0.23.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:31c583ba27426a3a04eca8c05450524105c1564db41be6632f7536ef405a6de2", size = 2394251, upload-time = "2025-08-06T14:58:01.725Z" }, + { url = "https://files.pythonhosted.org/packages/93/f3/3cdf55bbf0f737304d997561c34ab0176222e0496b6743b0feab5995182c/torchvision-0.23.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3932bf67256f2d095ce90a9f826f6033694c818856f4bb26794cf2ce64253e53", size = 8627497, upload-time = "2025-08-06T14:58:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/97/90/02afe57c3ef4284c5cf89d3b7ae203829b3a981f72b93a7dd2a3fd2c83c1/torchvision-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:83ee5bf827d61a8af14620c0a61d8608558638ac9c3bac8adb7b27138e2147d1", size = 1600760, upload-time = "2025-08-06T14:57:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d7/15d3d7bd8d0239211b21673d1bac7bc345a4ad904a8e25bb3fd8a9cf1fbc/torchvision-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49aa20e21f0c2bd458c71d7b449776cbd5f16693dd5807195a820612b8a229b7", size = 1856884, upload-time = "2025-08-06T14:58:00.237Z" }, + { url = "https://files.pythonhosted.org/packages/dd/14/7b44fe766b7d11e064c539d92a172fa9689a53b69029e24f2f1f51e7dc56/torchvision-0.23.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:01dc33ee24c79148aee7cdbcf34ae8a3c9da1674a591e781577b716d233b1fa6", size = 2395543, upload-time = "2025-08-06T14:58:04.373Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fcb09aff941c8147d9e6aa6c8f67412a05622b0c750bcf796be4c85a58d4/torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35c27941831b653f5101edfe62c03d196c13f32139310519e8228f35eae0e96a", size = 8628388, upload-time = "2025-08-06T14:58:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/93/40/3415d890eb357b25a8e0a215d32365a88ecc75a283f75c4e919024b22d97/torchvision-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:09bfde260e7963a15b80c9e442faa9f021c7e7f877ac0a36ca6561b367185013", size = 1600741, upload-time = "2025-08-06T14:57:59.158Z" }, + { url = "https://files.pythonhosted.org/packages/df/1d/0ea0b34bde92a86d42620f29baa6dcbb5c2fc85990316df5cb8f7abb8ea2/torchvision-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e0e2c04a91403e8dd3af9756c6a024a1d9c0ed9c0d592a8314ded8f4fe30d440", size = 1856885, upload-time = "2025-08-06T14:58:06.503Z" }, + { url = "https://files.pythonhosted.org/packages/e2/00/2f6454decc0cd67158c7890364e446aad4b91797087a57a78e72e1a8f8bc/torchvision-0.23.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6dd7c4d329a0e03157803031bc856220c6155ef08c26d4f5bbac938acecf0948", size = 2396614, upload-time = "2025-08-06T14:58:03.116Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b5/3e580dcbc16f39a324f3dd71b90edbf02a42548ad44d2b4893cc92b1194b/torchvision-0.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4e7d31c43bc7cbecbb1a5652ac0106b436aa66e26437585fc2c4b2cf04d6014c", size = 8627108, upload-time = "2025-08-06T14:58:12.956Z" }, + { url = "https://files.pythonhosted.org/packages/82/c1/c2fe6d61e110a8d0de2f94276899a2324a8f1e6aee559eb6b4629ab27466/torchvision-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2e45272abe7b8bf0d06c405e78521b5757be1bd0ed7e5cd78120f7fdd4cbf35", size = 1600723, upload-time = "2025-08-06T14:57:57.986Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3e/f1f3bb3dd452b98ec2eba4820d777440abceb3d3a428a6c8243006fe47e5/torchvision-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b190db205f90206c230fc2f91cbdfd5733334babc0e0d19bddb90a40b8cf26c2", size = 1856927, upload-time = "2025-08-06T14:58:18.919Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e2/aafc6af854e792d212ff58e459f8d5d807568dc3f2b49ec41b677275e5a9/torchvision-0.23.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6c74cbc1cbee26dd4f35f989cd80dccc40411f258dee476b29871dee4b483af0", size = 2392870, upload-time = "2025-08-06T14:58:21.303Z" }, + { url = "https://files.pythonhosted.org/packages/5d/06/09b6a917b3759ef000428af0aa2597f983e20d9fbbcfeb826750f778fe6d/torchvision-0.23.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:a9e9d7552d34547b80843eaf64ab0737b19b2e8bec2514286b8cfd30861ca8b5", size = 8630400, upload-time = "2025-08-06T14:58:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/08/07/ae46106efbf4bbc0090078aa4c406c38282cbe4e637bdb4b7f2e984140af/torchvision-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:dc7ce5accbbb8c9df9a79f8cef6a6df042f28e2250a6ae0d2ca70b06473fa03b", size = 1600751, upload-time = "2025-08-06T14:58:20.027Z" }, +] + +[[package]] +name = "torchvision" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.10'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +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" }, +] + +[[package]] +name = "tornado" +version = "6.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, + { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "trailrunner" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathspec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/93/630e10bacd897daeb9ff5a408f4e7cb0fc2f243e7e3ef00f9e6cf319b11c/trailrunner-1.4.0.tar.gz", hash = "sha256:3fe61e259e6b2e5192f321c265985b7a0dc18497ced62b2da244f08104978398", size = 15836, upload-time = "2023-03-27T07:54:35.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/29/21001afea86bac5016c3940b43de3ce4786b0d8337d4ea79bb903c649ce3/trailrunner-1.4.0-py3-none-any.whl", hash = "sha256:a286d39f2723f28d167347f41cf8f232832648709366e722f55cf5545772a48e", size = 11071, upload-time = "2023-03-27T07:54:32.514Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "triton" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "importlib-metadata", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "setuptools", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/ee/0ee5f64a87eeda19bbad9bc54ae5ca5b98186ed00055281fd40fb4beb10e/triton-3.4.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ff2785de9bc02f500e085420273bb5cc9c9bb767584a4aa28d6e360cec70128", size = 155430069, upload-time = "2025-07-30T19:58:21.715Z" }, + { url = "https://files.pythonhosted.org/packages/7d/39/43325b3b651d50187e591eefa22e236b2981afcebaefd4f2fc0ea99df191/triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b70f5e6a41e52e48cfc087436c8a28c17ff98db369447bcaff3b887a3ab4467", size = 155531138, upload-time = "2025-07-30T19:58:29.908Z" }, + { url = "https://files.pythonhosted.org/packages/d0/66/b1eb52839f563623d185f0927eb3530ee4d5ffe9d377cdaf5346b306689e/triton-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c1d84a5c0ec2c0f8e8a072d7fd150cab84a9c239eaddc6706c081bfae4eb04", size = 155560068, upload-time = "2025-07-30T19:58:37.081Z" }, + { url = "https://files.pythonhosted.org/packages/12/34/1251beb5a3cb93f3950ebe68732752014646003ef6eb11eb5f1a37ca78cd/triton-3.4.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98e5c1442eaeabae2e2452ae765801bd53cd4ce873cab0d1bdd59a32ab2d9397", size = 155430799, upload-time = "2025-07-30T19:58:57.664Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' 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" }, +] + +[[package]] +name = "typer-slim" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl", hash = "sha256:6e6c31047f171ac93cc5a973c9e617dbc5ab2bddc4d0a3135dc161b4e2020e0d", size = 47444, upload-time = "2026-01-06T11:21:12.441Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "ufmt" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "black" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "libcst" }, + { name = "moreorless" }, + { name = "tomlkit" }, + { name = "trailrunner" }, + { name = "typing-extensions" }, + { name = "usort" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/f8/c25e242a8e12062172dea4117859757a11339bbc39b1a3c7fb6a6de03bb2/ufmt-2.8.0.tar.gz", hash = "sha256:72c9502915497678de9aeab8aa18604890f14f869f7f378dd26e2878bde84f13", size = 24482, upload-time = "2024-10-25T06:21:57.239Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/4b/3f1b6f566b6cf70ccc5cba9a638fe4459f1e373c34d74df2e40e41871d70/ufmt-2.8.0-py3-none-any.whl", hash = "sha256:47a690811c576ebd3a0e30d77d43b65c84240e5c1611e5cb4a880bdd7f4507c1", size = 28268, upload-time = "2024-10-25T06:21:55.822Z" }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "usort" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "libcst" }, + { name = "moreorless" }, + { name = "stdlibs" }, + { name = "toml" }, + { name = "trailrunner" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/f6/0cf8fd139f3deab8f180a03062eb09d0bcfc380baf0df1ae24f82496577d/usort-1.0.2.tar.gz", hash = "sha256:f0dbdfcf18b117323dff3a03df804957ba3b755c1069d2cf98bee133592bd369", size = 78068, upload-time = "2022-03-07T22:09:08.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/9b/2967208157a740d24b5a059bc1371a97863fb4c392a6766fd473cf11fea9/usort-1.0.2-py3-none-any.whl", hash = "sha256:0e7ee0702902d4d54fdd35cbc81f5590df2573db29e72aeb6eddaa9e9d01cef9", size = 23261, upload-time = "2022-03-07T22:09:07.14Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/62/a7c072fbfefb2980a00f99ca994279cb9ecf310cb2e6b2a4d2a28fe192b3/wcwidth-0.5.3.tar.gz", hash = "sha256:53123b7af053c74e9fe2e92ac810301f6139e64379031f7124574212fb3b4091", size = 157587, upload-time = "2026-01-31T03:52:10.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/c1/d73f12f8cdb1891334a2ccf7389eed244d3941e74d80dd220badb937f3fb/wcwidth-0.5.3-py3-none-any.whl", hash = "sha256:d584eff31cd4753e1e5ff6c12e1edfdb324c995713f75d26c29807bb84bf649e", size = 92981, upload-time = "2026-01-31T03:52:09.14Z" }, +] + +[[package]] +name = "webcolors" +version = "24.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/29/061ec845fb58521848f3739e466efd8250b4b7b98c1b6c5bf4d40b419b7e/webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6", size = 45064, upload-time = "2024-11-11T07:43:24.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e8/c0e05e4684d13459f93d312077a9a2efbe04d59c393bc2b8802248c908d4/webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9", size = 14934, upload-time = "2024-11-11T07:43:22.529Z" }, +] + +[[package]] +name = "webcolors" +version = "25.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, +] + +[[package]] +name = "widgetsnbextension" +version = "4.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, +] + +[[package]] +name = "yacs" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/3e/4a45cb0738da6565f134c01d82ba291c746551b5bc82e781ec876eb20909/yacs-0.1.8.tar.gz", hash = "sha256:efc4c732942b3103bea904ee89af98bcd27d01f0ac12d8d4d369f1e7a2914384", size = 11100, upload-time = "2020-08-10T16:37:47.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/4f/fe9a4d472aa867878ce3bb7efb16654c5d63672b86dc0e6e953a67018433/yacs-0.1.8-py3-none-any.whl", hash = "sha256:99f893e30497a4b66842821bac316386f7bd5c4f47ad35c9073ef089aa33af32", size = 14747, upload-time = "2020-08-10T16:37:46.4Z" }, +] + +[[package]] +name = "yt-dlp" +version = "2025.10.14" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/03/b7/dab729345e22891e79294273bc59c5213a1ec87331f49cb82ccea2b1bc9f/yt_dlp-2025.10.14.tar.gz", hash = "sha256:b18436aa9bb6f04354fd78d31ad9eeaae8c81b6a859f07072b25c18cd6c25844", size = 3045272, upload-time = "2025-10-14T23:39:52.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/19/399c85d29bd7b366b31ede82698f7963374e5a3842ae9de0cde6514506b0/yt_dlp-2025.10.14-py3-none-any.whl", hash = "sha256:0b9da17eda1bbf48e2315130043d7993fd4ca1c5a35571f8231da1a910c9c115", size = 3248664, upload-time = "2025-10-14T23:39:49.95Z" }, +] + +[[package]] +name = "yt-dlp" +version = "2026.2.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.10.*' and sys_platform == 'darwin'", + "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/16/be/8e099f3f34bac6851490525fb1a8b62d525a95fcb5af082e8c52ba884fb5/yt_dlp-2026.2.4.tar.gz", hash = "sha256:24733ef081116f29d8ee6eae7a48127101e6c56eb7aa228dd604a60654760022", size = 3100305, upload-time = "2026-02-04T00:49:27.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/38/b17cbeaf6712a4c1b97f7f9ec3a55f3a8ddee678cc88742af47dca0315b7/yt_dlp-2026.2.4-py3-none-any.whl", hash = "sha256:d6ea83257e8127a0097b1d37ee36201f99a292067e4616b2e5d51ab153b3dbb9", size = 3299165, upload-time = "2026-02-04T00:49:25.31Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, + { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, + { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/d0a405dad6ab6f9f759c26d866cca66cb209bff6f8db656074d662a953dd/zstandard-0.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b9af1fe743828123e12b41dd8091eca1074d0c1569cc42e6e1eee98027f2bbd0", size = 795263, upload-time = "2025-09-14T22:18:21.683Z" }, + { url = "https://files.pythonhosted.org/packages/ca/aa/ceb8d79cbad6dabd4cb1178ca853f6a4374d791c5e0241a0988173e2a341/zstandard-0.25.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b14abacf83dfb5c25eb4e4a79520de9e7e205f72c9ee7702f91233ae57d33a2", size = 640560, upload-time = "2025-09-14T22:18:22.867Z" }, + { url = "https://files.pythonhosted.org/packages/88/cd/2cf6d476131b509cc122d25d3416a2d0aa17687ddbada7599149f9da620e/zstandard-0.25.0-cp39-cp39-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:a51ff14f8017338e2f2e5dab738ce1ec3b5a851f23b18c1ae1359b1eecbee6df", size = 5344244, upload-time = "2025-09-14T22:18:24.724Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/e14820b61a1c137966b7667b400b72fa4a45c836257e443f3d77607db268/zstandard-0.25.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3b870ce5a02d4b22286cf4944c628e0f0881b11b3f14667c1d62185a99e04f53", size = 5054550, upload-time = "2025-09-14T22:18:26.445Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ce/26dc5a6fa956be41d0e984909224ed196ee6f91d607f0b3fd84577741a77/zstandard-0.25.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:05353cef599a7b0b98baca9b068dd36810c3ef0f42bf282583f438caf6ddcee3", size = 5401150, upload-time = "2025-09-14T22:18:28.745Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1b/402cab5edcfe867465daf869d5ac2a94930931c0989633bc01d6a7d8bd68/zstandard-0.25.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19796b39075201d51d5f5f790bf849221e58b48a39a5fc74837675d8bafc7362", size = 5448595, upload-time = "2025-09-14T22:18:30.475Z" }, + { url = "https://files.pythonhosted.org/packages/86/b2/fc50c58271a1ead0e5a0a0e6311f4b221f35954dce438ce62751b3af9b68/zstandard-0.25.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53e08b2445a6bc241261fea89d065536f00a581f02535f8122eba42db9375530", size = 5555290, upload-time = "2025-09-14T22:18:32.336Z" }, + { url = "https://files.pythonhosted.org/packages/d2/20/5f72d6ba970690df90fdd37195c5caa992e70cb6f203f74cc2bcc0b8cf30/zstandard-0.25.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1f3689581a72eaba9131b1d9bdbfe520ccd169999219b41000ede2fca5c1bfdb", size = 5043898, upload-time = "2025-09-14T22:18:34.215Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f1/131a0382b8b8d11e84690574645f528f5c5b9343e06cefd77f5fd730cd2b/zstandard-0.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d8c56bb4e6c795fc77d74d8e8b80846e1fb8292fc0b5060cd8131d522974b751", size = 5571173, upload-time = "2025-09-14T22:18:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/53/f6/2a37931023f737fd849c5c28def57442bbafadb626da60cf9ed58461fe24/zstandard-0.25.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:53f94448fe5b10ee75d246497168e5825135d54325458c4bfffbaafabcc0a577", size = 4958261, upload-time = "2025-09-14T22:18:38.098Z" }, + { url = "https://files.pythonhosted.org/packages/b5/52/ca76ed6dbfd8845a5563d3af4e972da3b9da8a9308ca6b56b0b929d93e23/zstandard-0.25.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c2ba942c94e0691467ab901fc51b6f2085ff48f2eea77b1a48240f011e8247c7", size = 5265680, upload-time = "2025-09-14T22:18:39.834Z" }, + { url = "https://files.pythonhosted.org/packages/7a/59/edd117dedb97a768578b49fb2f1156defb839d1aa5b06200a62be943667f/zstandard-0.25.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:07b527a69c1e1c8b5ab1ab14e2afe0675614a09182213f21a0717b62027b5936", size = 5439747, upload-time = "2025-09-14T22:18:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/75/71/c2e9234643dcfbd6c5e975e9a2b0050e1b2afffda6c3a959e1b87997bc80/zstandard-0.25.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:51526324f1b23229001eb3735bc8c94f9c578b1bd9e867a0a646a3b17109f388", size = 5818805, upload-time = "2025-09-14T22:18:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/f5/93/8ebc19f0a31c44ea0e7348f9b0d4b326ed413b6575a3c6ff4ed50222abb6/zstandard-0.25.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89c4b48479a43f820b749df49cd7ba2dbc2b1b78560ecb5ab52985574fd40b27", size = 5362280, upload-time = "2025-09-14T22:18:45.625Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/29cc59d4a9d51b3fd8b477d858d0bd7ab627f700908bf1517f46ddd470ae/zstandard-0.25.0-cp39-cp39-win32.whl", hash = "sha256:1cd5da4d8e8ee0e88be976c294db744773459d51bb32f707a0f166e5ad5c8649", size = 436460, upload-time = "2025-09-14T22:18:49.077Z" }, + { url = "https://files.pythonhosted.org/packages/41/b5/bc7a92c116e2ef32dc8061c209d71e97ff6df37487d7d39adb51a343ee89/zstandard-0.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:37daddd452c0ffb65da00620afb8e17abd4adaae6ce6310702841760c2c26860", size = 506097, upload-time = "2025-09-14T22:18:47.342Z" }, +] From 5f70777a83b1cbbd0f4e1075f166cd23e114cb80 Mon Sep 17 00:00:00 2001 From: Ryan Avery Date: Fri, 6 Feb 2026 11:52:05 -0800 Subject: [PATCH 05/31] Stabilize artifact export and add benchmarking --- docs/export_progress.md | 42 +++++ scripts/benchmark_sam3_artifacts.py | 282 ++++++++++++++++++++++++++++ scripts/export_sam3_artifacts.py | 66 ++++--- scripts/test_sam3_artifacts.py | 86 +++++++-- tests/export/test_decoder_export.py | 107 ++++++----- 5 files changed, 486 insertions(+), 97 deletions(-) create mode 100644 docs/export_progress.md create mode 100644 scripts/benchmark_sam3_artifacts.py diff --git a/docs/export_progress.md b/docs/export_progress.md new file mode 100644 index 0000000..1380faf --- /dev/null +++ b/docs/export_progress.md @@ -0,0 +1,42 @@ +# Export Progress Summary + +## Current state + +- **Export scripts** now work end-to-end for the single-level path and produce PT2 + artifacts for image encoder, text encoder, encoder fusion, and decoder. +- **Test script** runs the exported artifacts, checks logits against eager, and + writes mask/box overlays. +- **Decoder export** is pinned to a **minimum batch of 2** to avoid export guard + failures. The test script pads inputs to batch >=2 when needed, then slices + outputs back to the original prompt count. + +## Changes made + +- `scripts/export_sam3_artifacts.py` + - Use single-level features (`backbone_fpn[-1]`) for encoder fusion export. + - Align image feature batch with prompt batch (repeat if needed). + - Use `torch.export.save(...)` for PT2 artifacts. + - Export decoder with a minimal prompt list (single prompt) and updated decoder + export logic to keep batch dynamic. + +- `scripts/test_sam3_artifacts.py` + - Use single-level features for encoder fusion. + - Repeat image features to match prompt batch for encoder fusion. + - Pad decoder inputs to batch >=2 if needed; compare against eager on the same + padded inputs and slice outputs back to the original prompt count. + - Resize masks to the input image for overlays and guard box drawing against + size mismatches. + +- `tests/export/test_decoder_export.py` + - Updated decoder export helper to attempt batch=1 (dynamic) and fall back to + batch=2. In practice, batch=2 is the stable path. + +- New script: `scripts/benchmark_sam3_artifacts.py` + - Benchmarks exported image encoder, text encoder, encoder fusion, decoder, and + compares to full eager inference time. + +## Open items + +- Decide whether to keep decoder export pinned to batch >=2 or revisit batch=1 + export constraints. +- Integrate the new benchmark script into docs or CI if desired. diff --git a/scripts/benchmark_sam3_artifacts.py b/scripts/benchmark_sam3_artifacts.py new file mode 100644 index 0000000..a9f7092 --- /dev/null +++ b/scripts/benchmark_sam3_artifacts.py @@ -0,0 +1,282 @@ +import argparse +import sys +import time +from pathlib import Path + +import numpy as np +import torch +from PIL import Image + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT)) + +from sam3.model_builder import build_sam3_image_model +from sam3.model.data_misc import FindStage +from sam3.model.geometry_encoders import Prompt + + +def _load_image(path: Path, device: torch.device) -> torch.Tensor: + image = Image.open(path).convert("RGB") + np_image = np.array(image, dtype=np.float32) / 255.0 + tensor = torch.from_numpy(np_image).permute(2, 0, 1).unsqueeze(0) + return tensor.to(device) + + +def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: + image = image.clamp(0, 1) + image = torch.nn.functional.interpolate( + image, size=(size, size), mode="bilinear", align_corners=False + ) + mean = torch.tensor([0.5, 0.5, 0.5], device=image.device).view(1, 3, 1, 1) + std = torch.tensor([0.5, 0.5, 0.5], device=image.device).view(1, 3, 1, 1) + return (image - mean) / std + + +def _make_inputs(model, image: torch.Tensor, prompts): + device = image.device + num_prompts = len(prompts) + + tokenizer = model.backbone.language_backbone.tokenizer + token_ids = tokenizer(prompts, context_length=32).to(device) + + img_ids = torch.zeros(num_prompts, device=device, dtype=torch.long) + text_ids = torch.zeros(num_prompts, device=device, dtype=torch.long) + + box_embeddings = torch.zeros(1, num_prompts, 4, device=device) + box_mask = torch.zeros(num_prompts, 1, device=device, dtype=torch.bool) + box_labels = torch.zeros(1, num_prompts, device=device, dtype=torch.long) + + return ( + image, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) + + +def _run_full_model(model, inputs): + ( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) = inputs + backbone_out = model.backbone.forward_image(images) + text_encoder = model.backbone.language_backbone + _, text_tokens = text_encoder.encoder(token_ids) + text_tokens = text_tokens.transpose(0, 1) + text_memory = text_encoder.resizer(text_tokens) + text_attention_mask = token_ids.ne(0) + text_attention_mask = text_attention_mask.ne(1) + backbone_out["language_features"] = text_memory + backbone_out["language_mask"] = text_attention_mask + + find_input = FindStage( + img_ids=img_ids, + text_ids=text_ids, + input_boxes=box_embeddings, + input_boxes_mask=box_mask, + input_boxes_label=box_labels, + input_points=torch.zeros(0, int(token_ids.shape[0]), 2, device=images.device), + input_points_mask=torch.zeros( + int(token_ids.shape[0]), 0, device=images.device, dtype=torch.bool + ), + ) + geometric_prompt = Prompt( + box_embeddings=box_embeddings, + box_mask=box_mask, + box_labels=box_labels, + ) + out = model.forward_grounding( + backbone_out=backbone_out, + find_input=find_input, + find_target=None, + geometric_prompt=geometric_prompt, + ) + return out["pred_masks"], out["pred_boxes"], out["pred_logits"] + + +def _load_export(path: Path): + exported = torch.export.load(str(path)) + return exported.module() + + +def _timeit(fn, iters: int, device: torch.device): + if device.type == "cuda": + torch.cuda.synchronize() + start = time.perf_counter() + for _ in range(iters): + fn() + if device.type == "cuda": + torch.cuda.synchronize() + end = time.perf_counter() + return (end - start) / iters + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--image", + type=Path, + default=Path("assets/images/cat_dog.jpg"), + help="Path to input image", + ) + parser.add_argument( + "--prompts", + type=str, + default="cat,dog", + help="Comma-separated text prompts", + ) + parser.add_argument( + "--device", + type=str, + default="cuda" if torch.cuda.is_available() else "cpu", + ) + parser.add_argument( + "--artifact-dir", + type=Path, + default=Path("artifacts/export"), + help="Directory with exported artifacts", + ) + parser.add_argument("--warmup", type=int, default=3) + parser.add_argument("--iters", type=int, default=10) + args = parser.parse_args() + + prompts = [p.strip() for p in args.prompts.split(",") if p.strip()] + if not prompts: + raise ValueError("Provide at least one prompt") + + model = build_sam3_image_model( + device=args.device, eval_mode=True, enable_segmentation=True + ) + model.eval() + + device = torch.device(args.device) + image = _load_image(args.image, device) + image = _prepare_image(image, size=1008) + inputs = _make_inputs(model, image, prompts) + + image_module = _load_export(args.artifact_dir / "image_encoder.pt2") + text_module = _load_export(args.artifact_dir / "text_encoder.pt2") + encoder_module = _load_export(args.artifact_dir / "encoder_fusion.pt2") + decoder_module = _load_export(args.artifact_dir / "decoder.pt2") + + with torch.no_grad(): + # Warmup + for _ in range(args.warmup): + _run_full_model(model, inputs) + vision_pos_enc = image_module(inputs[0])[1] + backbone_fpn = image_module(inputs[0])[2] + text_attention_mask, text_memory = text_module(inputs[1]) + img_feats = backbone_fpn[-1] + img_pos = vision_pos_enc[-1] + img_mask = torch.zeros( + img_feats.shape[0], + img_feats.shape[2], + img_feats.shape[3], + device=img_feats.device, + dtype=torch.bool, + ) + encoder_module( + img_feats, img_pos, img_mask, text_memory, text_attention_mask + ) + ( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) = inputs + if token_ids.shape[0] < 2: + repeat = 2 // token_ids.shape[0] + token_ids = token_ids.repeat(repeat, 1) + img_ids = img_ids.repeat(repeat) + text_ids = text_ids.repeat(repeat) + box_embeddings = box_embeddings.repeat(1, repeat, 1) + box_mask = box_mask.repeat(repeat, 1) + box_labels = box_labels.repeat(1, repeat) + decoder_module( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) + + def eager_fn(): + _run_full_model(model, inputs) + + def image_fn(): + image_module(inputs[0]) + + def text_fn(): + text_module(inputs[1]) + + def encoder_fn(): + vision_pos_enc = image_module(inputs[0])[1] + backbone_fpn = image_module(inputs[0])[2] + text_attention_mask, text_memory = text_module(inputs[1]) + img_feats = backbone_fpn[-1] + img_pos = vision_pos_enc[-1] + img_mask = torch.zeros( + img_feats.shape[0], + img_feats.shape[2], + img_feats.shape[3], + device=img_feats.device, + dtype=torch.bool, + ) + encoder_module(img_feats, img_pos, img_mask, text_memory, text_attention_mask) + + def decoder_fn(): + ( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) = inputs + if token_ids.shape[0] < 2: + repeat = 2 // token_ids.shape[0] + token_ids = token_ids.repeat(repeat, 1) + img_ids = img_ids.repeat(repeat) + text_ids = text_ids.repeat(repeat) + box_embeddings = box_embeddings.repeat(1, repeat, 1) + box_mask = box_mask.repeat(repeat, 1) + box_labels = box_labels.repeat(1, repeat) + decoder_module( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) + + eager_ms = _timeit(eager_fn, args.iters, device) * 1000 + image_ms = _timeit(image_fn, args.iters, device) * 1000 + text_ms = _timeit(text_fn, args.iters, device) * 1000 + encoder_ms = _timeit(encoder_fn, args.iters, device) * 1000 + decoder_ms = _timeit(decoder_fn, args.iters, device) * 1000 + + print("Eager total (ms):", round(eager_ms, 2)) + print("Export image encoder (ms):", round(image_ms, 2)) + print("Export text encoder (ms):", round(text_ms, 2)) + print("Export encoder fusion (ms):", round(encoder_ms, 2)) + print("Export decoder (ms):", round(decoder_ms, 2)) + + +if __name__ == "__main__": + main() diff --git a/scripts/export_sam3_artifacts.py b/scripts/export_sam3_artifacts.py index bf4a34f..3e59bb2 100644 --- a/scripts/export_sam3_artifacts.py +++ b/scripts/export_sam3_artifacts.py @@ -60,22 +60,7 @@ def _make_inputs(model, image: torch.Tensor, prompts): def _save_export(exported, path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) - exported.save(str(path)) - - -def _export_encoder_multilevel( - model, img_feats, img_pos, img_mask, prompt, prompt_mask -): - device = prompt.device - wrapper = EncoderFusionWrapper(model.transformer.encoder).to(device).eval() - with torch.no_grad(): - exported = torch.export.export( - wrapper, - (img_feats, img_pos, img_mask, prompt, prompt_mask), - strict=False, - prefer_deferred_runtime_asserts_over_guards=True, - ) - return exported + torch.export.save(exported, str(path)) def main() -> None: @@ -126,25 +111,46 @@ def main() -> None: with torch.no_grad(): image_module = image_encoder.module() text_module = text_encoder.module() - vision_features, vision_pos_enc, _ = image_module(inputs[0]) + _, vision_pos_enc, backbone_fpn = image_module(inputs[0]) text_attention_mask, text_memory = text_module(inputs[1]) prompt = text_memory prompt_mask = text_attention_mask - img_mask = [ - torch.zeros( - feat.shape[0], - feat.shape[2], - feat.shape[3], - device=feat.device, - dtype=torch.bool, - ) - for feat in vision_features - ] - encoder = _export_encoder_multilevel( - model, vision_features, vision_pos_enc, img_mask, prompt, prompt_mask + img_feats = backbone_fpn[-1] + img_pos = vision_pos_enc[-1] + img_mask = torch.zeros( + img_feats.shape[0], + img_feats.shape[2], + img_feats.shape[3], + device=img_feats.device, + dtype=torch.bool, + ) + prompt_batch = prompt.shape[1] + if img_feats.shape[0] != prompt_batch: + if img_feats.shape[0] != 1: + raise ValueError("Image batch does not match prompt batch") + img_feats = img_feats.repeat(prompt_batch, 1, 1, 1) + img_pos = img_pos.repeat(prompt_batch, 1, 1, 1) + img_mask = img_mask.repeat(prompt_batch, 1, 1) + + encoder_wrapper = ( + EncoderFusionWrapper(model.transformer.encoder).to(img_feats.device).eval() + ) + encoder = torch.export.export( + encoder_wrapper, + (img_feats, img_pos, img_mask, prompt, prompt_mask), + dynamic_shapes={ + "img_feats": {0: torch.export.Dim("batch", min=1, max=4)}, + "img_pos": {0: torch.export.Dim("batch", min=1, max=4)}, + "img_mask": {0: torch.export.Dim("batch", min=1, max=4)}, + "prompt": {0: 32, 1: torch.export.Dim("batch", min=1, max=4)}, + "prompt_mask": {0: torch.export.Dim("batch", min=1, max=4), 1: 32}, + }, + strict=False, + prefer_deferred_runtime_asserts_over_guards=True, ) print("Exporting decoder...") - decoder = _export_decoder(model, inputs) + decoder_inputs = _make_inputs(model, image, prompts[:1]) + decoder = _export_decoder(model, decoder_inputs) _save_export(image_encoder, args.out_dir / "image_encoder.pt2") _save_export(text_encoder, args.out_dir / "text_encoder.pt2") diff --git a/scripts/test_sam3_artifacts.py b/scripts/test_sam3_artifacts.py index dc83448..e45ed1b 100644 --- a/scripts/test_sam3_artifacts.py +++ b/scripts/test_sam3_artifacts.py @@ -136,16 +136,18 @@ def _overlay_masks( num_prompts, num_queries = scores.shape[:2] best_idx = scores.squeeze(-1).argmax(dim=1) colors = _color_palette(num_prompts) - base = image.copy() + base = image.copy().convert("RGBA") overlay = Image.new("RGBA", base.size, (0, 0, 0, 0)) for i in range(num_prompts): mask = masks[i, best_idx[i]].detach().cpu() mask = mask > 0 mask_img = Image.fromarray((mask.numpy() * 255).astype(np.uint8), mode="L") + if mask_img.size != base.size: + mask_img = mask_img.resize(base.size, resample=Image.Resampling.NEAREST) color = colors[i] color_img = Image.new("RGBA", base.size, (*color, 120)) overlay = Image.composite(color_img, overlay, mask_img) - blended = Image.alpha_composite(base.convert("RGBA"), overlay) + blended = Image.alpha_composite(base, overlay) blended.convert("RGB").save(out_path) @@ -153,11 +155,14 @@ def _draw_boxes( image: Image.Image, boxes_xyxy: torch.Tensor, scores: torch.Tensor, out_path: Path ): num_prompts, num_queries = scores.shape[:2] - best_idx = scores.squeeze(-1).argmax(dim=1) + best_idx = scores.squeeze(-1).argmax(dim=1).clamp(max=boxes_xyxy.shape[1] - 1) colors = _color_palette(num_prompts) draw = ImageDraw.Draw(image) for i in range(num_prompts): - box = boxes_xyxy[i, best_idx[i]].detach().cpu().tolist() + box_tensor = boxes_xyxy[i, best_idx[i]].detach().cpu().flatten() + if box_tensor.numel() != 4: + continue + box = box_tensor.tolist() color = colors[i] draw.rectangle(box, outline=color, width=3) image.save(out_path) @@ -193,6 +198,7 @@ def main() -> None: prompts = [p.strip() for p in args.prompts.split(",") if p.strip()] if not prompts: raise ValueError("Provide at least one prompt") + prompt_count = len(prompts) model = build_sam3_image_model( device=args.device, eval_mode=True, enable_segmentation=True @@ -214,29 +220,71 @@ def main() -> None: decoder_module = _load_export(args.artifact_dir / "decoder.pt2") with torch.no_grad(): - vision_features, vision_pos_enc, _ = image_module(inputs[0]) + _, vision_pos_enc, backbone_fpn = image_module(inputs[0]) text_attention_mask, text_memory = text_module(inputs[1]) - img_mask = [ - torch.zeros( - feat.shape[0], - feat.shape[2], - feat.shape[3], - device=feat.device, - dtype=torch.bool, - ) - for feat in vision_features - ] + img_feats = backbone_fpn[-1] + img_pos = vision_pos_enc[-1] + prompt_batch = text_attention_mask.shape[0] + if img_feats.shape[0] != prompt_batch: + if img_feats.shape[0] != 1: + raise ValueError("Image batch does not match prompt batch") + img_feats = img_feats.repeat(prompt_batch, 1, 1, 1) + img_pos = img_pos.repeat(prompt_batch, 1, 1, 1) + img_mask = torch.zeros( + img_feats.shape[0], + img_feats.shape[2], + img_feats.shape[3], + device=img_feats.device, + dtype=torch.bool, + ) enc_out = encoder_module( - vision_features, vision_pos_enc, img_mask, text_memory, text_attention_mask + img_feats, img_pos, img_mask, text_memory, text_attention_mask ) assert isinstance(enc_out, tuple) - pred_logits, pred_boxes, pred_masks, pred_boxes_xyxy = decoder_module(*inputs) + ( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) = inputs + if token_ids.shape[0] < 2: + repeat = 2 // token_ids.shape[0] + token_ids = token_ids.repeat(repeat, 1) + img_ids = img_ids.repeat(repeat) + text_ids = text_ids.repeat(repeat) + box_embeddings = box_embeddings.repeat(1, repeat, 1) + box_mask = box_mask.repeat(repeat, 1) + box_labels = box_labels.repeat(1, repeat) + decoder_inputs = ( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) + pred_logits, pred_boxes, pred_masks, pred_boxes_xyxy = decoder_module( + *decoder_inputs + ) + eager_ref_masks, eager_ref_boxes, eager_ref_logits, eager_ref_boxes_xyxy = ( + _run_full_model(model, decoder_inputs) + ) + + pred_logits = pred_logits[:prompt_count] + pred_boxes = pred_boxes[:prompt_count] + pred_masks = pred_masks[:prompt_count] + pred_boxes_xyxy = pred_boxes_xyxy[:prompt_count] + eager_ref_logits = eager_ref_logits[:prompt_count] - print("Prompt count:", len(prompts)) + print("Prompt count:", prompt_count) print("Pred logits shape:", pred_logits.shape) print("Pred boxes shape:", pred_boxes.shape) print("Pred masks shape:", pred_masks.shape) - torch.testing.assert_close(pred_logits, eager_logits, rtol=1e-3, atol=1e-3) + torch.testing.assert_close(pred_logits, eager_ref_logits, rtol=1e-3, atol=1e-3) print("Eager vs export logits match") out_dir = args.artifact_dir diff --git a/tests/export/test_decoder_export.py b/tests/export/test_decoder_export.py index e3680d3..4d38a2f 100644 --- a/tests/export/test_decoder_export.py +++ b/tests/export/test_decoder_export.py @@ -95,55 +95,66 @@ def _export_decoder(model: torch.nn.Module, inputs): ) = inputs device = images.device wrapper = FullInferenceWrapper(model).to(device).eval() # type: ignore[arg-type] - if images.shape[0] == 1: - images = images.repeat(2, 1, 1, 1) - token_ids = token_ids.repeat(2, 1) - img_ids = img_ids.repeat(2) - text_ids = text_ids.repeat(2) - box_embeddings = box_embeddings.repeat(1, 2, 1) - box_mask = box_mask.repeat(2, 1) - box_labels = box_labels.repeat(1, 2) - with torch.no_grad(): - exported = torch.export.export( - wrapper, - ( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, - ), - dynamic_shapes={ - "images": { - 0: torch.export.Dim.AUTO, - 2: 1008, - 3: 1008, - }, - "token_ids": { - 0: torch.export.Dim.AUTO, - 1: 32, - }, - "img_ids": {0: torch.export.Dim.AUTO}, - "text_ids": {0: torch.export.Dim.AUTO}, - "box_embeddings": { - 0: 1, - 1: torch.export.Dim.AUTO, - }, - "box_mask": { - 0: torch.export.Dim.AUTO, - 1: 1, - }, - "box_labels": { - 0: 1, - 1: torch.export.Dim.AUTO, + + def _export_with_min_batch(min_batch: int): + local_images = images + local_token_ids = token_ids + local_img_ids = img_ids + local_text_ids = text_ids + local_box_embeddings = box_embeddings + local_box_mask = box_mask + local_box_labels = box_labels + if local_images.shape[0] < min_batch: + repeat = min_batch // local_images.shape[0] + local_images = local_images.repeat(repeat, 1, 1, 1) + local_token_ids = local_token_ids.repeat(repeat, 1) + local_img_ids = local_img_ids.repeat(repeat) + local_text_ids = local_text_ids.repeat(repeat) + local_box_embeddings = local_box_embeddings.repeat(1, repeat, 1) + local_box_mask = local_box_mask.repeat(repeat, 1) + local_box_labels = local_box_labels.repeat(1, repeat) + with torch.no_grad(): + return torch.export.export( + wrapper, + ( + local_images, + local_token_ids, + local_img_ids, + local_text_ids, + local_box_embeddings, + local_box_mask, + local_box_labels, + ), + dynamic_shapes={ + "images": { + 0: torch.export.Dim.AUTO, + 2: 1008, + 3: 1008, + }, + "token_ids": { + 0: torch.export.Dim.AUTO, + 1: 32, + }, + "img_ids": {0: torch.export.Dim.AUTO}, + "text_ids": {0: torch.export.Dim.AUTO}, + "box_embeddings": { + 0: 1, + 1: torch.export.Dim.AUTO, + }, + "box_mask": { + 0: torch.export.Dim.AUTO, + 1: 1, + }, + "box_labels": { + 0: 1, + 1: torch.export.Dim.AUTO, + }, }, - }, - strict=False, - prefer_deferred_runtime_asserts_over_guards=True, - ) - return exported + strict=False, + prefer_deferred_runtime_asserts_over_guards=True, + ) + + return _export_with_min_batch(2) def test_decoder_export_static(sam3_model): From 7b9f84ad6abfc3f29474335ef227e443f2ef215c Mon Sep 17 00:00:00 2001 From: Ryan Avery Date: Fri, 6 Feb 2026 15:59:49 -0800 Subject: [PATCH 06/31] Align export scripts with decoder-only artifacts Simplify benchmarks/tests to avoid duplicated work and keep export usage consistent. --- scripts/benchmark_sam3_artifacts.py | 275 +++++++++++++++------- scripts/export_sam3_artifacts.py | 56 ++++- scripts/test_sam3_artifacts.py | 192 ++++++++++++++- tests/export/test_decoder_export.py | 227 +++++++++++++++--- tests/export/test_encoder_export.py | 27 --- tests/export/test_image_encoder_export.py | 34 --- tests/export/test_text_encoder_export.py | 44 +--- 7 files changed, 619 insertions(+), 236 deletions(-) diff --git a/scripts/benchmark_sam3_artifacts.py b/scripts/benchmark_sam3_artifacts.py index a9f7092..52381ec 100644 --- a/scripts/benchmark_sam3_artifacts.py +++ b/scripts/benchmark_sam3_artifacts.py @@ -35,12 +35,15 @@ def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: def _make_inputs(model, image: torch.Tensor, prompts): device = image.device num_prompts = len(prompts) + num_images = int(image.shape[0]) tokenizer = model.backbone.language_backbone.tokenizer token_ids = tokenizer(prompts, context_length=32).to(device) - img_ids = torch.zeros(num_prompts, device=device, dtype=torch.long) - text_ids = torch.zeros(num_prompts, device=device, dtype=torch.long) + img_ids = torch.arange(num_images, device=device, dtype=torch.long) + img_ids = img_ids.repeat_interleave(num_prompts) + text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) + text_ids = text_ids.repeat(num_images) box_embeddings = torch.zeros(1, num_prompts, 4, device=device) box_mask = torch.zeros(num_prompts, 1, device=device, dtype=torch.bool) @@ -102,6 +105,64 @@ def _run_full_model(model, inputs): return out["pred_masks"], out["pred_boxes"], out["pred_logits"] +def _make_decoder_only_inputs_from_model( + model, + backbone_fpn, + vision_pos_enc, + text_memory, + text_attention_mask, + inputs, +): + ( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) = inputs + backbone_out = { + "backbone_fpn": backbone_fpn, + "vision_pos_enc": vision_pos_enc, + "language_features": text_memory, + "language_mask": text_attention_mask, + } + find_input = FindStage( + img_ids=img_ids, + text_ids=text_ids, + input_boxes=box_embeddings, + input_boxes_mask=box_mask, + input_boxes_label=box_labels, + input_points=torch.zeros(0, int(token_ids.shape[0]), 2, device=images.device), + input_points_mask=torch.zeros( + int(token_ids.shape[0]), 0, device=images.device, dtype=torch.bool + ), + ) + geometric_prompt = Prompt( + box_embeddings=box_embeddings, + box_mask=box_mask, + box_labels=box_labels, + ) + prompt, prompt_mask, backbone_out = model._encode_prompt( + backbone_out, find_input, geometric_prompt + ) + backbone_out, encoder_out, _ = model._run_encoder( + backbone_out, find_input, prompt, prompt_mask + ) + return ( + backbone_out["backbone_fpn"], + img_ids, + encoder_out["encoder_hidden_states"], + encoder_out["pos_embed"], + prompt, + prompt_mask, + encoder_out["level_start_index"], + encoder_out["spatial_shapes"], + encoder_out["valid_ratios"], + ) + + def _load_export(path: Path): exported = torch.export.load(str(path)) return exported.module() @@ -162,72 +223,44 @@ def main() -> None: image = _prepare_image(image, size=1008) inputs = _make_inputs(model, image, prompts) - image_module = _load_export(args.artifact_dir / "image_encoder.pt2") - text_module = _load_export(args.artifact_dir / "text_encoder.pt2") - encoder_module = _load_export(args.artifact_dir / "encoder_fusion.pt2") - decoder_module = _load_export(args.artifact_dir / "decoder.pt2") - - with torch.no_grad(): - # Warmup - for _ in range(args.warmup): - _run_full_model(model, inputs) - vision_pos_enc = image_module(inputs[0])[1] - backbone_fpn = image_module(inputs[0])[2] - text_attention_mask, text_memory = text_module(inputs[1]) - img_feats = backbone_fpn[-1] - img_pos = vision_pos_enc[-1] - img_mask = torch.zeros( - img_feats.shape[0], - img_feats.shape[2], - img_feats.shape[3], - device=img_feats.device, - dtype=torch.bool, - ) - encoder_module( - img_feats, img_pos, img_mask, text_memory, text_attention_mask - ) - ( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, - ) = inputs - if token_ids.shape[0] < 2: - repeat = 2 // token_ids.shape[0] - token_ids = token_ids.repeat(repeat, 1) - img_ids = img_ids.repeat(repeat) - text_ids = text_ids.repeat(repeat) - box_embeddings = box_embeddings.repeat(1, repeat, 1) - box_mask = box_mask.repeat(repeat, 1) - box_labels = box_labels.repeat(1, repeat) - decoder_module( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, - ) + print("Device (eager):", next(model.parameters()).device) def eager_fn(): _run_full_model(model, inputs) + with torch.inference_mode(): + for _ in range(args.warmup): + eager_fn() + eager_ms = _timeit(eager_fn, args.iters, device) * 1000 + + if device.type == "cuda": + torch.cuda.empty_cache() + + image_module = _load_export(args.artifact_dir / "image_encoder.pt2") + text_module = _load_export(args.artifact_dir / "text_encoder.pt2") + encoder_module = _load_export(args.artifact_dir / "encoder_fusion.pt2") + pipeline_module = _load_export(args.artifact_dir / "full_sam3_pipeline.pt2") + decoder_module = _load_export(args.artifact_dir / "decoder_only.pt2") + print("Device (export):", inputs[0].device) + def image_fn(): - image_module(inputs[0]) + return image_module(inputs[0]) def text_fn(): - text_module(inputs[1]) + return text_module(inputs[1]) - def encoder_fn(): - vision_pos_enc = image_module(inputs[0])[1] - backbone_fpn = image_module(inputs[0])[2] - text_attention_mask, text_memory = text_module(inputs[1]) + def encoder_from_outputs(image_out, text_out): + vision_pos_enc = image_out[1] + backbone_fpn = image_out[2] + text_attention_mask, text_memory = text_out img_feats = backbone_fpn[-1] img_pos = vision_pos_enc[-1] + prompt_batch = text_attention_mask.shape[0] + if img_feats.shape[0] != prompt_batch: + if img_feats.shape[0] != 1: + raise ValueError("Image batch does not match prompt batch") + img_feats = img_feats.repeat(prompt_batch, 1, 1, 1) + img_pos = img_pos.repeat(prompt_batch, 1, 1, 1) img_mask = torch.zeros( img_feats.shape[0], img_feats.shape[2], @@ -237,45 +270,107 @@ def encoder_fn(): ) encoder_module(img_feats, img_pos, img_mask, text_memory, text_attention_mask) - def decoder_fn(): - ( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, - ) = inputs - if token_ids.shape[0] < 2: - repeat = 2 // token_ids.shape[0] - token_ids = token_ids.repeat(repeat, 1) - img_ids = img_ids.repeat(repeat) - text_ids = text_ids.repeat(repeat) - box_embeddings = box_embeddings.repeat(1, repeat, 1) - box_mask = box_mask.repeat(repeat, 1) - box_labels = box_labels.repeat(1, repeat) - decoder_module( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, + with torch.inference_mode(): + cached_image_out = image_fn() + cached_text_out = text_fn() + + def encoder_fn(): + encoder_from_outputs(cached_image_out, cached_text_out) + + ( + pipeline_images, + pipeline_token_ids, + pipeline_img_ids, + pipeline_text_ids, + pipeline_box_embeddings, + pipeline_box_mask, + pipeline_box_labels, + ) = inputs + if pipeline_token_ids.shape[0] < 2: + repeat = 2 // pipeline_token_ids.shape[0] + pipeline_token_ids = pipeline_token_ids.repeat(repeat, 1) + pipeline_img_ids = pipeline_img_ids.repeat(repeat) + pipeline_text_ids = pipeline_text_ids.repeat(repeat) + pipeline_box_embeddings = pipeline_box_embeddings.repeat(1, repeat, 1) + pipeline_box_mask = pipeline_box_mask.repeat(repeat, 1) + pipeline_box_labels = pipeline_box_labels.repeat(1, repeat) + pipeline_inputs = ( + pipeline_images, + pipeline_token_ids, + pipeline_img_ids, + pipeline_text_ids, + pipeline_box_embeddings, + pipeline_box_mask, + pipeline_box_labels, + ) + + def pipeline_fn(): + pipeline_module(*pipeline_inputs) + + with torch.inference_mode(): + cached_image_out = image_fn() + cached_text_out = text_fn() + decoder_only_inputs = _make_decoder_only_inputs_from_model( + model, + cached_image_out[2], + cached_image_out[1], + cached_text_out[1], + cached_text_out[0], + inputs, ) + ( + decoder_backbone_fpn, + decoder_img_ids, + decoder_memory, + decoder_pos_embed, + decoder_prompt, + decoder_prompt_mask, + decoder_level_start_index, + decoder_spatial_shapes, + decoder_valid_ratios, + ) = decoder_only_inputs + if decoder_img_ids.shape[0] < 2: + repeat = 2 // decoder_img_ids.shape[0] + decoder_img_ids = decoder_img_ids.repeat(repeat) + decoder_memory = decoder_memory.repeat(1, repeat, 1) + decoder_pos_embed = decoder_pos_embed.repeat(1, repeat, 1) + decoder_prompt = decoder_prompt.repeat(1, repeat, 1) + decoder_prompt_mask = decoder_prompt_mask.repeat(repeat, 1) + decoder_valid_ratios = decoder_valid_ratios.repeat(repeat, 1, 1) + decoder_backbone_fpn = [ + feat.repeat(repeat, 1, 1, 1) for feat in decoder_backbone_fpn + ] + decoder_only_inputs = ( + decoder_backbone_fpn, + decoder_img_ids, + decoder_memory, + decoder_pos_embed, + decoder_prompt, + decoder_prompt_mask, + decoder_level_start_index, + decoder_spatial_shapes, + decoder_valid_ratios, + ) + + def decoder_only_fn(): + decoder_module(*decoder_only_inputs) - eager_ms = _timeit(eager_fn, args.iters, device) * 1000 - image_ms = _timeit(image_fn, args.iters, device) * 1000 - text_ms = _timeit(text_fn, args.iters, device) * 1000 - encoder_ms = _timeit(encoder_fn, args.iters, device) * 1000 - decoder_ms = _timeit(decoder_fn, args.iters, device) * 1000 + with torch.inference_mode(): + for _ in range(args.warmup): + pipeline_fn() + image_ms = _timeit(image_fn, args.iters, device) * 1000 + text_ms = _timeit(text_fn, args.iters, device) * 1000 + encoder_ms = _timeit(encoder_fn, args.iters, device) * 1000 + pipeline_ms = _timeit(pipeline_fn, args.iters, device) * 1000 + decoder_only_ms = _timeit(decoder_only_fn, args.iters, device) * 1000 print("Eager total (ms):", round(eager_ms, 2)) + print("Export full pipeline total (ms):", round(pipeline_ms, 2)) print("Export image encoder (ms):", round(image_ms, 2)) print("Export text encoder (ms):", round(text_ms, 2)) print("Export encoder fusion (ms):", round(encoder_ms, 2)) - print("Export decoder (ms):", round(decoder_ms, 2)) + print("Export decoder only (ms):", round(decoder_only_ms, 2)) + del model if __name__ == "__main__": diff --git a/scripts/export_sam3_artifacts.py b/scripts/export_sam3_artifacts.py index 3e59bb2..8f25647 100644 --- a/scripts/export_sam3_artifacts.py +++ b/scripts/export_sam3_artifacts.py @@ -10,7 +10,11 @@ sys.path.insert(0, str(REPO_ROOT)) from sam3.model_builder import build_sam3_image_model -from tests.export.test_decoder_export import _export_decoder +from tests.export.test_decoder_export import ( + _export_decoder_only, + _export_full_sam3_pipeline, + _make_decoder_only_inputs, +) from tests.export.test_encoder_export import EncoderFusionWrapper from tests.export.test_image_encoder_export import _export_image_encoder from tests.export.test_text_encoder_export import _export_text_encoder @@ -36,12 +40,15 @@ def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: def _make_inputs(model, image: torch.Tensor, prompts): device = image.device num_prompts = len(prompts) + num_images = int(image.shape[0]) tokenizer = model.backbone.language_backbone.tokenizer token_ids = tokenizer(prompts, context_length=32).to(device) - img_ids = torch.zeros(num_prompts, device=device, dtype=torch.long) - text_ids = torch.zeros(num_prompts, device=device, dtype=torch.long) + img_ids = torch.arange(num_images, device=device, dtype=torch.long) + img_ids = img_ids.repeat_interleave(num_prompts) + text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) + text_ids = text_ids.repeat(num_images) box_embeddings = torch.zeros(1, num_prompts, 4, device=device) box_mask = torch.zeros(num_prompts, 1, device=device, dtype=torch.bool) @@ -148,14 +155,49 @@ def main() -> None: strict=False, prefer_deferred_runtime_asserts_over_guards=True, ) - print("Exporting decoder...") - decoder_inputs = _make_inputs(model, image, prompts[:1]) - decoder = _export_decoder(model, decoder_inputs) + print("Exporting full pipeline...") + pipeline_inputs = _make_inputs(model, image, prompts[:1]) + full_pipeline = _export_full_sam3_pipeline(model, pipeline_inputs) + print("Exporting decoder only...") + decoder_inputs = _make_decoder_only_inputs(model, pipeline_inputs) + ( + backbone_fpn, + img_ids, + memory, + pos_embed, + prompt, + prompt_mask, + level_start_index, + spatial_shapes, + valid_ratios, + ) = decoder_inputs + if img_ids.shape[0] < 2: + repeat = 2 // img_ids.shape[0] + img_ids = img_ids.repeat(repeat) + memory = memory.repeat(1, repeat, 1) + pos_embed = pos_embed.repeat(1, repeat, 1) + prompt = prompt.repeat(1, repeat, 1) + prompt_mask = prompt_mask.repeat(repeat, 1) + valid_ratios = valid_ratios.repeat(repeat, 1, 1) + backbone_fpn = [feat.repeat(repeat, 1, 1, 1) for feat in backbone_fpn] + decoder_inputs = ( + backbone_fpn, + img_ids, + memory, + pos_embed, + prompt, + prompt_mask, + level_start_index, + spatial_shapes, + valid_ratios, + ) + decoder_only = _export_decoder_only(model, decoder_inputs) _save_export(image_encoder, args.out_dir / "image_encoder.pt2") _save_export(text_encoder, args.out_dir / "text_encoder.pt2") _save_export(encoder, args.out_dir / "encoder_fusion.pt2") - _save_export(decoder, args.out_dir / "decoder.pt2") + _save_export(full_pipeline, args.out_dir / "full_sam3_pipeline.pt2") + _save_export(decoder_only, args.out_dir / "decoder_only.pt2") print("Saved exports to", args.out_dir) diff --git a/scripts/test_sam3_artifacts.py b/scripts/test_sam3_artifacts.py index e45ed1b..dd8348b 100644 --- a/scripts/test_sam3_artifacts.py +++ b/scripts/test_sam3_artifacts.py @@ -15,13 +15,20 @@ from sam3.model.geometry_encoders import Prompt -def _load_image(path: Path, device: torch.device) -> torch.Tensor: - image = Image.open(path).convert("RGB") +def _load_pil_image(path: Path) -> Image.Image: + return Image.open(path).convert("RGB") + + +def _pil_to_tensor(image: Image.Image, device: torch.device) -> torch.Tensor: np_image = np.array(image, dtype=np.float32) / 255.0 tensor = torch.from_numpy(np_image).permute(2, 0, 1).unsqueeze(0) return tensor.to(device) +def _load_image(path: Path, device: torch.device) -> torch.Tensor: + return _pil_to_tensor(_load_pil_image(path), device) + + def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: image = image.clamp(0, 1) image = torch.nn.functional.interpolate( @@ -35,12 +42,15 @@ def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: def _make_inputs(model, image: torch.Tensor, prompts): device = image.device num_prompts = len(prompts) + num_images = int(image.shape[0]) tokenizer = model.backbone.language_backbone.tokenizer token_ids = tokenizer(prompts, context_length=32).to(device) - img_ids = torch.zeros(num_prompts, device=device, dtype=torch.long) - text_ids = torch.zeros(num_prompts, device=device, dtype=torch.long) + img_ids = torch.arange(num_images, device=device, dtype=torch.long) + img_ids = img_ids.repeat_interleave(num_prompts) + text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) + text_ids = text_ids.repeat(num_images) box_embeddings = torch.zeros(1, num_prompts, 4, device=device) box_mask = torch.zeros(num_prompts, 1, device=device, dtype=torch.bool) @@ -107,6 +117,64 @@ def _run_full_model(model, inputs): ) +def _make_decoder_only_inputs_from_model( + model, + backbone_fpn, + vision_pos_enc, + text_memory, + text_attention_mask, + inputs, +): + ( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) = inputs + backbone_out = { + "backbone_fpn": backbone_fpn, + "vision_pos_enc": vision_pos_enc, + "language_features": text_memory, + "language_mask": text_attention_mask, + } + find_input = FindStage( + img_ids=img_ids, + text_ids=text_ids, + input_boxes=box_embeddings, + input_boxes_mask=box_mask, + input_boxes_label=box_labels, + input_points=torch.zeros(0, int(token_ids.shape[0]), 2, device=images.device), + input_points_mask=torch.zeros( + int(token_ids.shape[0]), 0, device=images.device, dtype=torch.bool + ), + ) + geometric_prompt = Prompt( + box_embeddings=box_embeddings, + box_mask=box_mask, + box_labels=box_labels, + ) + prompt, prompt_mask, backbone_out = model._encode_prompt( + backbone_out, find_input, geometric_prompt + ) + backbone_out, encoder_out, _ = model._run_encoder( + backbone_out, find_input, prompt, prompt_mask + ) + return ( + backbone_out["backbone_fpn"], + img_ids, + encoder_out["encoder_hidden_states"], + encoder_out["pos_embed"], + prompt, + prompt_mask, + encoder_out["level_start_index"], + encoder_out["spatial_shapes"], + encoder_out["valid_ratios"], + ) + + def _load_export(path: Path): exported = torch.export.load(str(path)) return exported.module() @@ -163,6 +231,20 @@ def _draw_boxes( if box_tensor.numel() != 4: continue box = box_tensor.tolist() + width, height = image.size + if max(box) <= 1.0: + box = [ + box[0] * width, + box[1] * height, + box[2] * width, + box[3] * height, + ] + box = [ + max(0.0, min(box[0], width)), + max(0.0, min(box[1], height)), + max(0.0, min(box[2], width)), + max(0.0, min(box[3], height)), + ] color = colors[i] draw.rectangle(box, outline=color, width=3) image.save(out_path) @@ -205,7 +287,8 @@ def main() -> None: ) model.eval() - image = _load_image(args.image, torch.device(args.device)) + pil_image = _load_pil_image(args.image) + image = _pil_to_tensor(pil_image, torch.device(args.device)) image = _prepare_image(image, size=1008) inputs = _make_inputs(model, image, prompts) @@ -217,7 +300,8 @@ def main() -> None: image_module = _load_export(args.artifact_dir / "image_encoder.pt2") text_module = _load_export(args.artifact_dir / "text_encoder.pt2") encoder_module = _load_export(args.artifact_dir / "encoder_fusion.pt2") - decoder_module = _load_export(args.artifact_dir / "decoder.pt2") + pipeline_module = _load_export(args.artifact_dir / "full_sam3_pipeline.pt2") + decoder_module = _load_export(args.artifact_dir / "decoder_only.pt2") with torch.no_grad(): _, vision_pos_enc, backbone_fpn = image_module(inputs[0]) @@ -267,8 +351,49 @@ def main() -> None: box_mask, box_labels, ) + pipeline_logits, pipeline_boxes, pipeline_masks, pipeline_boxes_xyxy = ( + pipeline_module(*decoder_inputs) + ) + ( + decoder_backbone_fpn, + decoder_img_ids, + decoder_memory, + decoder_pos_embed, + decoder_prompt, + decoder_prompt_mask, + decoder_level_start_index, + decoder_spatial_shapes, + decoder_valid_ratios, + ) = _make_decoder_only_inputs_from_model( + model, + backbone_fpn, + vision_pos_enc, + text_memory, + text_attention_mask, + inputs, + ) + batch_target = decoder_img_ids.shape[0] + if batch_target < 2: + repeat = 2 // batch_target + decoder_img_ids = decoder_img_ids.repeat(repeat) + decoder_memory = decoder_memory.repeat(1, repeat, 1) + decoder_pos_embed = decoder_pos_embed.repeat(1, repeat, 1) + decoder_prompt = decoder_prompt.repeat(1, repeat, 1) + decoder_prompt_mask = decoder_prompt_mask.repeat(repeat, 1) + decoder_valid_ratios = decoder_valid_ratios.repeat(repeat, 1, 1) + decoder_backbone_fpn = [ + feat.repeat(repeat, 1, 1, 1) for feat in decoder_backbone_fpn + ] pred_logits, pred_boxes, pred_masks, pred_boxes_xyxy = decoder_module( - *decoder_inputs + decoder_backbone_fpn, + decoder_img_ids, + decoder_memory, + decoder_pos_embed, + decoder_prompt, + decoder_prompt_mask, + decoder_level_start_index, + decoder_spatial_shapes, + decoder_valid_ratios, ) eager_ref_masks, eager_ref_boxes, eager_ref_logits, eager_ref_boxes_xyxy = ( _run_full_model(model, decoder_inputs) @@ -278,41 +403,88 @@ def main() -> None: pred_boxes = pred_boxes[:prompt_count] pred_masks = pred_masks[:prompt_count] pred_boxes_xyxy = pred_boxes_xyxy[:prompt_count] + pipeline_logits = pipeline_logits[:prompt_count] + pipeline_masks = pipeline_masks[:prompt_count] + pipeline_boxes_xyxy = pipeline_boxes_xyxy[:prompt_count] eager_ref_logits = eager_ref_logits[:prompt_count] print("Prompt count:", prompt_count) print("Pred logits shape:", pred_logits.shape) print("Pred boxes shape:", pred_boxes.shape) print("Pred masks shape:", pred_masks.shape) + pred_scores = pred_logits.squeeze(-1) + eager_scores = eager_ref_logits.squeeze(-1) + pred_max = pred_scores.max(dim=1).values + eager_max = eager_scores.max(dim=1).values + pred_best_idx = pred_scores.argmax(dim=1) + eager_best_idx = eager_scores.argmax(dim=1) + for idx, prompt_text in enumerate(prompts): + print( + f"Prompt '{prompt_text}' max logit: " + f"export={pred_max[idx].item():.4f} (idx {pred_best_idx[idx].item()}), " + f"eager={eager_max[idx].item():.4f} (idx {eager_best_idx[idx].item()})" + ) torch.testing.assert_close(pred_logits, eager_ref_logits, rtol=1e-3, atol=1e-3) + torch.testing.assert_close(pipeline_logits, eager_ref_logits, rtol=1e-3, atol=1e-3) print("Eager vs export logits match") out_dir = args.artifact_dir out_dir.mkdir(parents=True, exist_ok=True) - base_image = _to_pil_image(inputs[0][0]) + base_image = pil_image.copy() _overlay_masks( base_image.copy(), eager_masks, eager_logits, out_dir / "eager_masks_overlay.jpg", ) + _overlay_masks( + base_image.copy(), + pipeline_masks, + pipeline_logits, + out_dir / "pipeline_masks_overlay.jpg", + ) _overlay_masks( base_image.copy(), pred_masks, pred_logits, - out_dir / "export_masks_overlay.jpg", + out_dir / "decoder_only_masks_overlay.jpg", ) + for idx, prompt_text in enumerate(prompts): + _overlay_masks( + base_image.copy(), + eager_masks[idx : idx + 1], + eager_logits[idx : idx + 1], + out_dir / f"eager_mask_overlay_{idx}.jpg", + ) + _overlay_masks( + base_image.copy(), + pipeline_masks[idx : idx + 1], + pipeline_logits[idx : idx + 1], + out_dir / f"pipeline_mask_overlay_{idx}.jpg", + ) + _overlay_masks( + base_image.copy(), + pred_masks[idx : idx + 1], + pred_logits[idx : idx + 1], + out_dir / f"decoder_only_mask_overlay_{idx}.jpg", + ) _draw_boxes( base_image.copy(), eager_boxes_xyxy, eager_logits, out_dir / "eager_boxes_overlay.jpg", ) + _draw_boxes( + base_image.copy(), + pipeline_boxes_xyxy, + pipeline_logits, + out_dir / "pipeline_boxes_overlay.jpg", + ) _draw_boxes( base_image.copy(), pred_boxes_xyxy, pred_logits, - out_dir / "export_boxes_overlay.jpg", + out_dir / "decoder_only_boxes_overlay.jpg", ) print("Saved overlays to", out_dir) diff --git a/tests/export/test_decoder_export.py b/tests/export/test_decoder_export.py index 4d38a2f..234f576 100644 --- a/tests/export/test_decoder_export.py +++ b/tests/export/test_decoder_export.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any, cast + import pytest import torch @@ -8,8 +10,8 @@ from tests.export.utils import capture_stderr_on_fail, get_device -class FullInferenceWrapper(torch.nn.Module): - def __init__(self, model: torch.nn.Module): +class FullSam3PipelineWrapper(torch.nn.Module): + def __init__(self, model: Any): super().__init__() self.model = model @@ -23,7 +25,7 @@ def forward( box_mask: torch.Tensor, box_labels: torch.Tensor, ): - model = self.model # type: ignore[assignment] + model = cast(Any, self.model) backbone_out = model.backbone.forward_image(images) text_encoder = model.backbone.language_backbone _, text_tokens = text_encoder.encoder(token_ids) @@ -83,7 +85,65 @@ def _make_inputs(batch: int, height: int, width: int, device: str, num_boxes: in return images, token_ids, img_ids, text_ids, box_embeddings, box_mask, box_labels -def _export_decoder(model: torch.nn.Module, inputs): +def _make_decoder_only_inputs(model: Any, inputs): + ( + images, + token_ids, + img_ids, + text_ids, + box_embeddings, + box_mask, + box_labels, + ) = inputs + model_any = cast(Any, model) + model_any = model_any.eval() + backbone_out = model_any.backbone.forward_image(images) + text_encoder = model_any.backbone.language_backbone + _, text_tokens = text_encoder.encoder(token_ids) + text_tokens = text_tokens.transpose(0, 1) + text_memory = text_encoder.resizer(text_tokens) + text_attention_mask = token_ids.ne(0) + text_attention_mask = text_attention_mask.ne(1) + backbone_out["language_features"] = text_memory + backbone_out["language_mask"] = text_attention_mask + + find_input = FindStage( + img_ids=img_ids, + text_ids=text_ids, + input_boxes=box_embeddings, + input_boxes_mask=box_mask, + input_boxes_label=box_labels, + input_points=torch.zeros(0, int(token_ids.shape[0]), 2, device=images.device), + input_points_mask=torch.zeros( + int(token_ids.shape[0]), 0, device=images.device, dtype=torch.bool + ), + ) + geometric_prompt = Prompt( + box_embeddings=box_embeddings, + box_mask=box_mask, + box_labels=box_labels, + ) + + prompt, prompt_mask, backbone_out = model_any._encode_prompt( + backbone_out, find_input, geometric_prompt + ) + backbone_out, encoder_out, _ = model_any._run_encoder( + backbone_out, find_input, prompt, prompt_mask + ) + return ( + backbone_out["backbone_fpn"], + img_ids, + encoder_out["encoder_hidden_states"], + encoder_out["pos_embed"], + prompt, + prompt_mask, + encoder_out["level_start_index"], + encoder_out["spatial_shapes"], + encoder_out["valid_ratios"], + ) + + +def _export_full_sam3_pipeline(model: Any, inputs): ( images, token_ids, @@ -94,7 +154,7 @@ def _export_decoder(model: torch.nn.Module, inputs): box_labels, ) = inputs device = images.device - wrapper = FullInferenceWrapper(model).to(device).eval() # type: ignore[arg-type] + wrapper = FullSam3PipelineWrapper(model).to(device).eval() # type: ignore[arg-type] def _export_with_min_batch(min_batch: int): local_images = images @@ -157,11 +217,113 @@ def _export_with_min_batch(min_batch: int): return _export_with_min_batch(2) +class DecoderOnlyWrapper(torch.nn.Module): + def __init__(self, model: torch.nn.Module): + super().__init__() + self.model = model + + def forward( + self, + backbone_fpn, + img_ids: torch.Tensor, + memory: torch.Tensor, + pos_embed: torch.Tensor, + prompt: torch.Tensor, + prompt_mask: torch.Tensor, + level_start_index: torch.Tensor, + spatial_shapes: torch.Tensor, + valid_ratios: torch.Tensor, + ): + model = cast(Any, self.model) + vis_feat_sizes = [(feat.shape[-2], feat.shape[-1]) for feat in backbone_fpn] + encoder_out = { + "pos_embed": pos_embed, + "padding_mask": None, + "level_start_index": level_start_index, + "spatial_shapes": spatial_shapes, + "valid_ratios": valid_ratios, + "vis_feat_sizes": vis_feat_sizes, + } + out = {"encoder_hidden_states": memory} + out, hs = model._run_decoder( + memory=memory, + pos_embed=pos_embed, + src_mask=None, + out=out, + prompt=prompt, + prompt_mask=prompt_mask, + encoder_out=encoder_out, + ) + backbone_out = {"backbone_fpn": backbone_fpn} + model._run_segmentation_heads( + out=out, + backbone_out=backbone_out, + img_ids=img_ids, + vis_feat_sizes=vis_feat_sizes, + encoder_hidden_states=out["encoder_hidden_states"], + prompt=prompt, + prompt_mask=prompt_mask, + hs=hs, + ) + return ( + out["pred_logits"], + out["pred_boxes"], + out["pred_masks"], + out["pred_boxes_xyxy"], + ) + + +def _export_decoder_only(model: Any, inputs): + ( + backbone_fpn, + img_ids, + memory, + pos_embed, + prompt, + prompt_mask, + level_start_index, + spatial_shapes, + valid_ratios, + ) = inputs + device = memory.device + wrapper = DecoderOnlyWrapper(model).to(device).eval() # type: ignore[arg-type] + dynamic_shapes = [ + [{0: torch.export.Dim.AUTO} for _ in backbone_fpn], + {0: torch.export.Dim.AUTO}, + {1: torch.export.Dim.AUTO}, + {1: torch.export.Dim.AUTO}, + {1: torch.export.Dim.AUTO}, + {0: torch.export.Dim.AUTO}, + {}, + {}, + {0: torch.export.Dim.AUTO}, + ] + with torch.no_grad(): + exported = torch.export.export( + wrapper, + ( + backbone_fpn, + img_ids, + memory, + pos_embed, + prompt, + prompt_mask, + level_start_index, + spatial_shapes, + valid_ratios, + ), + dynamic_shapes=dynamic_shapes, + strict=False, + prefer_deferred_runtime_asserts_over_guards=True, + ) + return exported + + def test_decoder_export_static(sam3_model): device = get_device() inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) with capture_stderr_on_fail("export_static"): - exported = _export_decoder(sam3_model, inputs) + exported = _export_full_sam3_pipeline(sam3_model, inputs) assert exported is not None @@ -169,7 +331,7 @@ def test_decoder_export_loads(sam3_model): device = get_device() inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) with capture_stderr_on_fail("export_loads"): - exported = _export_decoder(sam3_model, inputs) + exported = _export_full_sam3_pipeline(sam3_model, inputs) module = exported.module() with torch.no_grad(): out = module(*inputs) @@ -180,11 +342,11 @@ def test_decoder_export_loads(sam3_model): def test_decoder_export_matches_eager(sam3_model): device = get_device() inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) - wrapper = FullInferenceWrapper(sam3_model).to(device).eval() + wrapper = FullSam3PipelineWrapper(sam3_model).to(device).eval() with torch.no_grad(): eager_out = wrapper(*inputs) with capture_stderr_on_fail("export_match"): - exported = _export_decoder(sam3_model, inputs) + exported = _export_full_sam3_pipeline(sam3_model, inputs) module = exported.module() with torch.no_grad(): export_out = module(*inputs) @@ -195,50 +357,43 @@ def test_decoder_export_matches_eager(sam3_model): torch.testing.assert_close(eager, compiled, rtol=1e-3, atol=1e-3) -def test_decoder_export_dynamic_batch(sam3_model): +@pytest.mark.parametrize("batch", [1, 2]) +def test_full_sam3_pipeline_export_inference_shapes(sam3_model, batch: int): device = get_device() inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) - with capture_stderr_on_fail("export_dynamic_batch"): - exported = _export_decoder(sam3_model, inputs) + with capture_stderr_on_fail("export_inference_shapes"): + exported = _export_full_sam3_pipeline(sam3_model, inputs) module = exported.module() - new_inputs = _make_inputs(2, 1008, 1008, device, num_boxes=1) + new_inputs = _make_inputs(batch, 1008, 1008, device, num_boxes=1) with torch.no_grad(): out = module(*new_inputs) assert isinstance(out, tuple) -def test_decoder_export_dynamic_spatial(sam3_model): +def test_decoder_only_export_loads(sam3_model): device = get_device() inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) - with capture_stderr_on_fail("export_dynamic_spatial"): - exported = _export_decoder(sam3_model, inputs) + decoder_inputs = _make_decoder_only_inputs(sam3_model, inputs) + with capture_stderr_on_fail("export_decoder_only_loads"): + exported = _export_decoder_only(sam3_model, decoder_inputs) module = exported.module() - new_inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) with torch.no_grad(): - out = module(*new_inputs) + out = module(*decoder_inputs) assert isinstance(out, tuple) + assert len(out) == 4 -def test_decoder_export_full_dynamic(sam3_model): +def test_decoder_only_export_matches_eager(sam3_model): device = get_device() inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) - with capture_stderr_on_fail("export_full_dynamic"): - exported = _export_decoder(sam3_model, inputs) - module = exported.module() - new_inputs = _make_inputs(2, 1008, 1008, device, num_boxes=1) + decoder_inputs = _make_decoder_only_inputs(sam3_model, inputs) + wrapper = DecoderOnlyWrapper(sam3_model).to(device).eval() with torch.no_grad(): - out = module(*new_inputs) - assert isinstance(out, tuple) - - -@pytest.mark.parametrize("batch", [1, 2]) -def test_decoder_export_inference_shapes(sam3_model, batch: int): - device = get_device() - inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) - with capture_stderr_on_fail("export_inference_shapes"): - exported = _export_decoder(sam3_model, inputs) + eager_out = wrapper(*decoder_inputs) + with capture_stderr_on_fail("export_decoder_only_match"): + exported = _export_decoder_only(sam3_model, decoder_inputs) module = exported.module() - new_inputs = _make_inputs(batch, 1008, 1008, device, num_boxes=1) with torch.no_grad(): - out = module(*new_inputs) - assert isinstance(out, tuple) + export_out = module(*decoder_inputs) + for eager, compiled in zip(eager_out, export_out): + torch.testing.assert_close(eager, compiled, rtol=1e-3, atol=1e-3) diff --git a/tests/export/test_encoder_export.py b/tests/export/test_encoder_export.py index 6316d2c..65f7f08 100644 --- a/tests/export/test_encoder_export.py +++ b/tests/export/test_encoder_export.py @@ -137,33 +137,6 @@ def test_encoder_export_matches_eager(sam3_model): torch.testing.assert_close(eager, compiled, rtol=1e-3, atol=1e-3) -def test_encoder_export_dynamic_batch(sam3_model): - device = get_device() - img_feats, img_pos, img_mask = _make_image_tokens(1, 72, 72, device) - prompt, prompt_mask = _make_prompt(1, 4, device) - with capture_stderr_on_fail("export_dynamic_batch"): - exported = _export_encoder(sam3_model, img_feats, img_pos, prompt, prompt_mask) - module = exported.module() - img_feats2, img_pos2, img_mask2 = _make_image_tokens(2, 72, 72, device) - prompt2, prompt_mask2 = _make_prompt(2, 4, device) - with torch.no_grad(): - out = module(img_feats2, img_pos2, img_mask2, prompt2, prompt_mask2) - assert isinstance(out, tuple) - - -def test_encoder_export_dynamic_spatial(sam3_model): - device = get_device() - img_feats, img_pos, img_mask = _make_image_tokens(1, 72, 72, device) - prompt, prompt_mask = _make_prompt(1, 4, device) - with capture_stderr_on_fail("export_dynamic_spatial"): - exported = _export_encoder(sam3_model, img_feats, img_pos, prompt, prompt_mask) - module = exported.module() - img_feats2, img_pos2, img_mask2 = _make_image_tokens(1, 72, 72, device) - with torch.no_grad(): - out = module(img_feats2, img_pos2, img_mask2, prompt, prompt_mask) - assert isinstance(out, tuple) - - @pytest.mark.parametrize("batch,seq_len", [(1, 4), (2, 8)]) def test_encoder_export_inference_shapes(sam3_model, batch: int, seq_len: int): device = get_device() diff --git a/tests/export/test_image_encoder_export.py b/tests/export/test_image_encoder_export.py index 58aa4fb..b97281f 100644 --- a/tests/export/test_image_encoder_export.py +++ b/tests/export/test_image_encoder_export.py @@ -87,40 +87,6 @@ def test_image_encoder_export_matches_eager(sam3_model): torch.testing.assert_close(eager, compiled, rtol=1e-3, atol=1e-3) -def test_image_encoder_export_dynamic_batch(sam3_model): - device = get_device() - images = _make_images(1, 1008, 1008, device) - with capture_stderr_on_fail("export_dynamic_batch"): - exported = _export_image_encoder(sam3_model, images) - module = exported.module() - with torch.no_grad(): - out = module(_make_images(2, 1008, 1008, device)) - assert isinstance(out, tuple) - - -@pytest.mark.parametrize("height,width", [(1008, 1008)]) -def test_image_encoder_export_dynamic_spatial(sam3_model, height: int, width: int): - device = get_device() - images = _make_images(1, 1008, 1008, device) - with capture_stderr_on_fail("export_dynamic_spatial"): - exported = _export_image_encoder(sam3_model, images) - module = exported.module() - with torch.no_grad(): - out = module(_make_images(1, height, width, device)) - assert isinstance(out, tuple) - - -def test_image_encoder_export_full_dynamic(sam3_model): - device = get_device() - images = _make_images(1, 1008, 1008, device) - with capture_stderr_on_fail("export_full_dynamic"): - exported = _export_image_encoder(sam3_model, images) - module = exported.module() - with torch.no_grad(): - out = module(_make_images(3, 1008, 1008, device)) - assert isinstance(out, tuple) - - @pytest.mark.parametrize("batch,height,width", [(2, 1008, 1008)]) def test_image_encoder_export_inference_shapes( sam3_model, batch: int, height: int, width: int diff --git a/tests/export/test_text_encoder_export.py b/tests/export/test_text_encoder_export.py index 402ba8c..83c14df 100644 --- a/tests/export/test_text_encoder_export.py +++ b/tests/export/test_text_encoder_export.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any, cast + import pytest import torch @@ -21,15 +23,19 @@ def forward(self, token_ids: torch.Tensor): return text_attention_mask, text_memory -def _make_tokens(batch: int, seq_len: int, vocab_size: int, device: str) -> torch.Tensor: +def _make_tokens( + batch: int, seq_len: int, vocab_size: int, device: str +) -> torch.Tensor: token_ids = torch.randint(0, vocab_size, (batch, seq_len), device=device) token_ids[:, -1] = 1 return token_ids -def _export_text_encoder(model: torch.nn.Module, token_ids: torch.Tensor): +def _export_text_encoder(model: Any, token_ids: torch.Tensor): device = token_ids.device - wrapper = TextEncoderWrapper(model.backbone.language_backbone).to(device).eval() # type: ignore[arg-type] + model_any = cast(Any, model) + text_encoder = cast(VETextEncoder, model_any.backbone.language_backbone) + wrapper = TextEncoderWrapper(text_encoder).to(device).eval() export_tokens = token_ids if token_ids.shape[0] == 1: export_tokens = token_ids.repeat(2, 1) @@ -75,7 +81,8 @@ def test_text_encoder_export_matches_eager(sam3_model): device = get_device() vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size token_ids = _make_tokens(1, 32, vocab_size, device) - wrapper = TextEncoderWrapper(sam3_model.backbone.language_backbone).to(device).eval() # type: ignore[arg-type] + text_encoder = cast(VETextEncoder, sam3_model.backbone.language_backbone) + wrapper = TextEncoderWrapper(text_encoder).to(device).eval() with torch.no_grad(): eager_out = wrapper(token_ids) with capture_stderr_on_fail("export_match"): @@ -87,35 +94,8 @@ def test_text_encoder_export_matches_eager(sam3_model): torch.testing.assert_close(eager_out[1], export_out[1], rtol=1e-3, atol=1e-3) -def test_text_encoder_export_dynamic_batch(sam3_model): - device = get_device() - vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size - token_ids = _make_tokens(1, 32, vocab_size, device) - with capture_stderr_on_fail("export_dynamic_batch"): - exported = _export_text_encoder(sam3_model, token_ids) - module = exported.module() - with torch.no_grad(): - out = module(_make_tokens(2, 32, vocab_size, device)) - assert isinstance(out, tuple) - - -@pytest.mark.parametrize("seq_len", [32]) -def test_text_encoder_export_dynamic_seq_len(sam3_model, seq_len: int): - device = get_device() - vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size - token_ids = _make_tokens(1, 32, vocab_size, device) - with capture_stderr_on_fail("export_dynamic_seq_len"): - exported = _export_text_encoder(sam3_model, token_ids) - module = exported.module() - with torch.no_grad(): - out = module(_make_tokens(1, seq_len, vocab_size, device)) - assert isinstance(out, tuple) - - @pytest.mark.parametrize("batch,seq_len", [(1, 32), (2, 32)]) -def test_text_encoder_export_inference_shapes( - sam3_model, batch: int, seq_len: int -): +def test_text_encoder_export_inference_shapes(sam3_model, batch: int, seq_len: int): device = get_device() vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size token_ids = _make_tokens(1, 32, vocab_size, device) From daa7294ce7c019ba94c9fa513bb6d8e89eae3102 Mon Sep 17 00:00:00 2001 From: Ryan Avery Date: Fri, 6 Feb 2026 16:52:18 -0800 Subject: [PATCH 07/31] Add standalone full pipeline export and timing helpers Introduce minimal export/test scripts for the full pipeline and add dedicated export/load timing benchmarks. --- scripts/benchmark_sam3_export_times.py | 143 +++++++++++++++++ scripts/benchmark_sam3_load_time.py | 40 +++++ scripts/export_sam3_full_pipeline.py | 206 +++++++++++++++++++++++++ scripts/test_sam3_full_pipeline.py | 112 ++++++++++++++ tests/export/test_decoder_export.py | 103 ++++++------- 5 files changed, 545 insertions(+), 59 deletions(-) create mode 100644 scripts/benchmark_sam3_export_times.py create mode 100644 scripts/benchmark_sam3_load_time.py create mode 100644 scripts/export_sam3_full_pipeline.py create mode 100644 scripts/test_sam3_full_pipeline.py diff --git a/scripts/benchmark_sam3_export_times.py b/scripts/benchmark_sam3_export_times.py new file mode 100644 index 0000000..2b8bcf6 --- /dev/null +++ b/scripts/benchmark_sam3_export_times.py @@ -0,0 +1,143 @@ +import argparse +import sys +import time +from pathlib import Path + +import numpy as np +import torch +from PIL import Image + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT)) + +from sam3.model_builder import build_sam3_image_model +from tests.export.test_decoder_export import ( + _export_decoder_only, + _export_full_sam3_pipeline, + _make_decoder_only_inputs, + _make_inputs, +) +from tests.export.test_encoder_export import EncoderFusionWrapper +from tests.export.test_image_encoder_export import _export_image_encoder +from tests.export.test_text_encoder_export import _export_text_encoder + + +def _load_image(path: Path, device: torch.device) -> torch.Tensor: + image = Image.open(path).convert("RGB") + np_image = np.array(image, dtype=np.float32) / 255.0 + return torch.from_numpy(np_image).permute(2, 0, 1).unsqueeze(0).to(device) + + +def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: + image = image.clamp(0, 1) + image = torch.nn.functional.interpolate( + image, size=(size, size), mode="bilinear", align_corners=False + ) + mean = torch.tensor([0.5, 0.5, 0.5], device=image.device).view(1, 3, 1, 1) + std = torch.tensor([0.5, 0.5, 0.5], device=image.device).view(1, 3, 1, 1) + return (image - mean) / std + + +def _time(label: str, fn) -> float: + start = time.perf_counter() + fn() + elapsed = time.perf_counter() - start + print(f"{label}: {elapsed:.3f}s") + return elapsed + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--image", + type=Path, + default=Path("assets/images/cat_dog.jpg"), + help="Path to input image", + ) + parser.add_argument( + "--device", + type=str, + default="cuda" if torch.cuda.is_available() else "cpu", + ) + args = parser.parse_args() + + device = torch.device(args.device) + model = build_sam3_image_model( + device=args.device, eval_mode=True, enable_segmentation=True + ) + model.eval() + + image = _prepare_image(_load_image(args.image, device), size=1008) + inputs = _make_inputs(1, 1008, 1008, str(device), num_boxes=1) + + with torch.no_grad(): + backbone_out = model.backbone.forward_image(image) + text_encoder = model.backbone.language_backbone + _, text_tokens = text_encoder.encoder(inputs[1]) + text_tokens = text_tokens.transpose(0, 1) + text_memory = text_encoder.resizer(text_tokens) + text_attention_mask = inputs[1].ne(0) + text_attention_mask = text_attention_mask.ne(1) + img_feats = backbone_out["backbone_fpn"][-1] + img_pos = backbone_out["vision_pos_enc"][-1] + if img_feats.shape[0] < 2: + repeat = 2 // img_feats.shape[0] + img_feats = img_feats.repeat(repeat, 1, 1, 1) + img_pos = img_pos.repeat(repeat, 1, 1, 1) + text_memory = text_memory.repeat(1, repeat, 1) + text_attention_mask = text_attention_mask.repeat(repeat, 1) + img_mask = torch.zeros( + img_feats.shape[0], + img_feats.shape[2], + img_feats.shape[3], + device=img_feats.device, + dtype=torch.bool, + ) + decoder_inputs = _make_decoder_only_inputs(model, inputs) + + def export_image_encoder(): + _export_image_encoder(model, image) + + def export_text_encoder(): + _export_text_encoder(model, inputs[1]) + + def export_encoder_fusion(): + encoder_wrapper = ( + EncoderFusionWrapper(model.transformer.encoder).to(img_feats.device).eval() + ) + torch.export.export( + encoder_wrapper, + (img_feats, img_pos, img_mask, text_memory, text_attention_mask), + dynamic_shapes={ + "img_feats": {0: torch.export.Dim.AUTO}, + "img_pos": {0: torch.export.Dim.AUTO}, + "img_mask": {0: torch.export.Dim.AUTO}, + "prompt": { + 0: torch.export.Dim("seq", min=1, max=64), + 1: torch.export.Dim.AUTO, + }, + "prompt_mask": { + 0: torch.export.Dim.AUTO, + 1: torch.export.Dim("seq", min=1, max=64), + }, + }, + strict=False, + prefer_deferred_runtime_asserts_over_guards=True, + ) + + def export_full_pipeline(): + _export_full_sam3_pipeline(model, inputs) + + def export_decoder_only(): + _export_decoder_only(model, decoder_inputs) + + with torch.no_grad(): + _time("Export image encoder", export_image_encoder) + _time("Export text encoder", export_text_encoder) + _time("Export encoder fusion", export_encoder_fusion) + _time("Export full pipeline", export_full_pipeline) + _time("Export decoder only", export_decoder_only) + + +if __name__ == "__main__": + main() diff --git a/scripts/benchmark_sam3_load_time.py b/scripts/benchmark_sam3_load_time.py new file mode 100644 index 0000000..77ea5f8 --- /dev/null +++ b/scripts/benchmark_sam3_load_time.py @@ -0,0 +1,40 @@ +import argparse +import sys +import time +from pathlib import Path + +import torch +import torchvision.ops # noqa: F401 + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT)) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--artifact", + type=Path, + default=Path("artifacts/export/full_sam3_pipeline.pt2"), + help="Path to exported full pipeline artifact", + ) + parser.add_argument( + "--device", + type=str, + default="cuda" if torch.cuda.is_available() else "cpu", + ) + args = parser.parse_args() + + if args.device.startswith("cuda"): + torch.cuda.synchronize() + start = time.perf_counter() + module = torch.export.load(str(args.artifact)).module() + module.to(args.device) + if args.device.startswith("cuda"): + torch.cuda.synchronize() + elapsed = time.perf_counter() - start + print(f"Load full pipeline to {args.device}: {elapsed:.3f}s") + + +if __name__ == "__main__": + main() diff --git a/scripts/export_sam3_full_pipeline.py b/scripts/export_sam3_full_pipeline.py new file mode 100644 index 0000000..b9dc5c1 --- /dev/null +++ b/scripts/export_sam3_full_pipeline.py @@ -0,0 +1,206 @@ +import argparse +import sys +from pathlib import Path + +import numpy as np +import torch +from PIL import Image + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT)) + +from sam3.model_builder import build_sam3_image_model +from sam3.model.data_misc import FindStage +from sam3.model.geometry_encoders import Prompt + + +class FullSam3PipelineWrapper(torch.nn.Module): + def __init__(self, model: torch.nn.Module): + super().__init__() + self.model = model + + def forward( + self, + images: torch.Tensor, + token_ids: torch.Tensor, + img_ids: torch.Tensor, + text_ids: torch.Tensor, + box_embeddings: torch.Tensor, + box_mask: torch.Tensor, + box_labels: torch.Tensor, + ): + model = self.model + backbone_out = model.backbone.forward_image(images) + text_encoder = model.backbone.language_backbone + _, text_tokens = text_encoder.encoder(token_ids) + text_tokens = text_tokens.transpose(0, 1) + text_memory = text_encoder.resizer(text_tokens) + text_attention_mask = token_ids.ne(0) + text_attention_mask = text_attention_mask.ne(1) + backbone_out["language_features"] = text_memory + backbone_out["language_mask"] = text_attention_mask + + find_input = FindStage( + img_ids=img_ids, + text_ids=text_ids, + input_boxes=box_embeddings, + input_boxes_mask=box_mask, + input_boxes_label=box_labels, + input_points=torch.zeros( + 0, int(token_ids.shape[0]), 2, device=images.device + ), + input_points_mask=torch.zeros( + int(token_ids.shape[0]), 0, device=images.device, dtype=torch.bool + ), + ) + geometric_prompt = Prompt( + box_embeddings=box_embeddings, + box_mask=box_mask, + box_labels=box_labels, + ) + out = model.forward_grounding( + backbone_out=backbone_out, + find_input=find_input, + find_target=None, + geometric_prompt=geometric_prompt, + ) + return ( + out["pred_logits"], + out["pred_boxes"], + out["pred_masks"], + out.get("presence_logit_dec"), + ) + + +def _load_image(path: Path, device: torch.device) -> torch.Tensor: + image = Image.open(path).convert("RGB") + np_image = np.array(image, dtype=np.float32) / 255.0 + return torch.from_numpy(np_image).permute(2, 0, 1).unsqueeze(0).to(device) + + +def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: + image = image.clamp(0, 1) + image = torch.nn.functional.interpolate( + image, size=(size, size), mode="bilinear", align_corners=False + ) + mean = torch.tensor([0.5, 0.5, 0.5], device=image.device).view(1, 3, 1, 1) + std = torch.tensor([0.5, 0.5, 0.5], device=image.device).view(1, 3, 1, 1) + return (image - mean) / std + + +def _make_inputs(model, image: torch.Tensor, prompts): + device = image.device + num_prompts = len(prompts) + num_images = int(image.shape[0]) + token_ids = model.backbone.language_backbone.tokenizer( + prompts, context_length=32 + ).to(device) + img_ids = torch.arange(num_images, device=device, dtype=torch.long) + img_ids = img_ids.repeat_interleave(num_prompts) + text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) + text_ids = text_ids.repeat(num_images) + return ( + image, + token_ids, + img_ids, + text_ids, + torch.zeros(1, num_prompts, 4, device=device), + torch.zeros(num_prompts, 1, device=device, dtype=torch.bool), + torch.zeros(1, num_prompts, device=device, dtype=torch.long), + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--image", + type=Path, + default=Path("assets/images/cat_dog.jpg"), + help="Path to input image", + ) + parser.add_argument( + "--prompts", + type=str, + default="cat,dog", + help="Comma-separated text prompts", + ) + parser.add_argument( + "--device", + type=str, + default="cuda" if torch.cuda.is_available() else "cpu", + ) + parser.add_argument( + "--out-dir", + type=Path, + default=Path("artifacts/export"), + help="Directory to write exported artifact", + ) + args = parser.parse_args() + + prompts = [p.strip() for p in args.prompts.split(",") if p.strip()] + if not prompts: + raise ValueError("Provide at least one prompt") + + model = build_sam3_image_model( + device=args.device, eval_mode=True, enable_segmentation=True + ) + model.eval() + + image = _prepare_image( + _load_image(args.image, torch.device(args.device)), size=1008 + ) + inputs = _make_inputs(model, image, prompts) + wrapper = FullSam3PipelineWrapper(model).to(image.device).eval() + if image.shape[0] < 2: + repeat = 2 // image.shape[0] + export_inputs = ( + image.repeat(repeat, 1, 1, 1), + inputs[1].repeat(repeat, 1), + inputs[2].repeat(repeat), + inputs[3].repeat(repeat), + inputs[4].repeat(1, repeat, 1), + inputs[5].repeat(repeat, 1), + inputs[6].repeat(1, repeat), + ) + else: + export_inputs = inputs + with torch.no_grad(): + exported = torch.export.export( + wrapper, + export_inputs, + dynamic_shapes={ + "images": { + 0: torch.export.Dim.AUTO, + 2: 1008, + 3: 1008, + }, + "token_ids": { + 0: torch.export.Dim.AUTO, + 1: 32, + }, + "img_ids": {0: torch.export.Dim.AUTO}, + "text_ids": {0: torch.export.Dim.AUTO}, + "box_embeddings": { + 0: 1, + 1: torch.export.Dim.AUTO, + }, + "box_mask": { + 0: torch.export.Dim.AUTO, + 1: 1, + }, + "box_labels": { + 0: 1, + 1: torch.export.Dim.AUTO, + }, + }, + strict=False, + prefer_deferred_runtime_asserts_over_guards=True, + ) + out_path = args.out_dir / "full_sam3_pipeline.pt2" + out_path.parent.mkdir(parents=True, exist_ok=True) + torch.export.save(exported, str(out_path)) + print("Saved export to", out_path) + + +if __name__ == "__main__": + main() diff --git a/scripts/test_sam3_full_pipeline.py b/scripts/test_sam3_full_pipeline.py new file mode 100644 index 0000000..b5db78c --- /dev/null +++ b/scripts/test_sam3_full_pipeline.py @@ -0,0 +1,112 @@ +import argparse +import sys +from pathlib import Path + +import numpy as np +import torch +from PIL import Image + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT)) + +from sam3.model_builder import build_sam3_image_model + + +def _load_image(path: Path, device: torch.device) -> torch.Tensor: + image = Image.open(path).convert("RGB") + np_image = np.array(image, dtype=np.float32) / 255.0 + return torch.from_numpy(np_image).permute(2, 0, 1).unsqueeze(0).to(device) + + +def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: + image = image.clamp(0, 1) + image = torch.nn.functional.interpolate( + image, size=(size, size), mode="bilinear", align_corners=False + ) + mean = torch.tensor([0.5, 0.5, 0.5], device=image.device).view(1, 3, 1, 1) + std = torch.tensor([0.5, 0.5, 0.5], device=image.device).view(1, 3, 1, 1) + return (image - mean) / std + + +def _make_inputs(model, image: torch.Tensor, prompts): + device = image.device + num_prompts = len(prompts) + num_images = int(image.shape[0]) + token_ids = model.backbone.language_backbone.tokenizer( + prompts, context_length=32 + ).to(device) + img_ids = torch.arange(num_images, device=device, dtype=torch.long) + img_ids = img_ids.repeat_interleave(num_prompts) + text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) + text_ids = text_ids.repeat(num_images) + return ( + image, + token_ids, + img_ids, + text_ids, + torch.zeros(1, num_prompts, 4, device=device), + torch.zeros(num_prompts, 1, device=device, dtype=torch.bool), + torch.zeros(1, num_prompts, device=device, dtype=torch.long), + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--image", + type=Path, + default=Path("assets/images/cat_dog.jpg"), + help="Path to input image", + ) + parser.add_argument( + "--prompts", + type=str, + default="cat,dog", + help="Comma-separated text prompts", + ) + parser.add_argument( + "--device", + type=str, + default="cuda" if torch.cuda.is_available() else "cpu", + ) + parser.add_argument( + "--artifact", + type=Path, + default=Path("artifacts/export/full_sam3_pipeline.pt2"), + help="Path to exported full pipeline artifact", + ) + args = parser.parse_args() + + prompts = [p.strip() for p in args.prompts.split(",") if p.strip()] + if not prompts: + raise ValueError("Provide at least one prompt") + + model = build_sam3_image_model( + device=args.device, eval_mode=True, enable_segmentation=True + ) + model.eval() + + image = _prepare_image( + _load_image(args.image, torch.device(args.device)), size=1008 + ) + inputs = _make_inputs(model, image, prompts) + + module = torch.export.load(str(args.artifact)).module() + with torch.no_grad(): + pred_logits, pred_boxes, pred_masks, pred_presence = module(*inputs) + + pred_scores = pred_logits.squeeze(-1) + print("Prompt count:", len(prompts)) + print("Pred logits shape:", pred_logits.shape) + print("Pred boxes shape:", pred_boxes.shape) + print("Pred masks shape:", pred_masks.shape) + if pred_presence is not None: + print("Presence logits shape:", pred_presence.shape) + for idx, prompt_text in enumerate(prompts): + max_val = pred_scores[idx].max().item() + max_idx = pred_scores[idx].argmax().item() + print(f"Prompt '{prompt_text}' max logit: {max_val:.4f} (idx {max_idx})") + + +if __name__ == "__main__": + main() diff --git a/tests/export/test_decoder_export.py b/tests/export/test_decoder_export.py index 234f576..ca769b5 100644 --- a/tests/export/test_decoder_export.py +++ b/tests/export/test_decoder_export.py @@ -155,66 +155,51 @@ def _export_full_sam3_pipeline(model: Any, inputs): ) = inputs device = images.device wrapper = FullSam3PipelineWrapper(model).to(device).eval() # type: ignore[arg-type] - - def _export_with_min_batch(min_batch: int): - local_images = images - local_token_ids = token_ids - local_img_ids = img_ids - local_text_ids = text_ids - local_box_embeddings = box_embeddings - local_box_mask = box_mask - local_box_labels = box_labels - if local_images.shape[0] < min_batch: - repeat = min_batch // local_images.shape[0] - local_images = local_images.repeat(repeat, 1, 1, 1) - local_token_ids = local_token_ids.repeat(repeat, 1) - local_img_ids = local_img_ids.repeat(repeat) - local_text_ids = local_text_ids.repeat(repeat) - local_box_embeddings = local_box_embeddings.repeat(1, repeat, 1) - local_box_mask = local_box_mask.repeat(repeat, 1) - local_box_labels = local_box_labels.repeat(1, repeat) - with torch.no_grad(): - return torch.export.export( - wrapper, - ( - local_images, - local_token_ids, - local_img_ids, - local_text_ids, - local_box_embeddings, - local_box_mask, - local_box_labels, - ), - dynamic_shapes={ - "images": { - 0: torch.export.Dim.AUTO, - 2: 1008, - 3: 1008, - }, - "token_ids": { - 0: torch.export.Dim.AUTO, - 1: 32, - }, - "img_ids": {0: torch.export.Dim.AUTO}, - "text_ids": {0: torch.export.Dim.AUTO}, - "box_embeddings": { - 0: 1, - 1: torch.export.Dim.AUTO, - }, - "box_mask": { - 0: torch.export.Dim.AUTO, - 1: 1, - }, - "box_labels": { - 0: 1, - 1: torch.export.Dim.AUTO, - }, + if images.shape[0] < 2: + repeat = 2 // images.shape[0] + export_inputs = ( + images.repeat(repeat, 1, 1, 1), + token_ids.repeat(repeat, 1), + img_ids.repeat(repeat), + text_ids.repeat(repeat), + box_embeddings.repeat(1, repeat, 1), + box_mask.repeat(repeat, 1), + box_labels.repeat(1, repeat), + ) + else: + export_inputs = inputs + with torch.no_grad(): + return torch.export.export( + wrapper, + export_inputs, + dynamic_shapes={ + "images": { + 0: torch.export.Dim.AUTO, + 2: 1008, + 3: 1008, }, - strict=False, - prefer_deferred_runtime_asserts_over_guards=True, - ) - - return _export_with_min_batch(2) + "token_ids": { + 0: torch.export.Dim.AUTO, + 1: 32, + }, + "img_ids": {0: torch.export.Dim.AUTO}, + "text_ids": {0: torch.export.Dim.AUTO}, + "box_embeddings": { + 0: 1, + 1: torch.export.Dim.AUTO, + }, + "box_mask": { + 0: torch.export.Dim.AUTO, + 1: 1, + }, + "box_labels": { + 0: 1, + 1: torch.export.Dim.AUTO, + }, + }, + strict=False, + prefer_deferred_runtime_asserts_over_guards=True, + ) class DecoderOnlyWrapper(torch.nn.Module): From 732232b7713cc984ff5d742538ff55ac96e4c007 Mon Sep 17 00:00:00 2001 From: Ryan Avery Date: Fri, 6 Feb 2026 17:28:13 -0800 Subject: [PATCH 08/31] Add feature-level control to SAM3 and timing helpers Allow configuring num_feature_levels for image models and add/extend export, load, and inference benchmarks. --- .gitignore | 2 ++ sam3/model_builder.py | 26 +++++++++++----- scripts/benchmark_sam3_artifacts.py | 11 ++++++- scripts/benchmark_sam3_export_times.py | 43 +++++++++++++++++++++++--- scripts/benchmark_sam3_load_time.py | 6 ++++ scripts/export_sam3_full_pipeline.py | 15 +++++++-- scripts/test_sam3_full_pipeline.py | 11 ++++++- 7 files changed, 98 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index fcda494..2a64259 100644 --- a/.gitignore +++ b/.gitignore @@ -124,12 +124,14 @@ dmypy.json # Model weights and checkpoints *.pth *.pt +*.pt2 *.bin *.ckpt *.safetensors weights/ checkpoints/ sam3_logs/ +artifacts/ # Data files *.h5 diff --git a/sam3/model_builder.py b/sam3/model_builder.py index 103b324..4548234 100644 --- a/sam3/model_builder.py +++ b/sam3/model_builder.py @@ -115,7 +115,9 @@ def _create_vl_backbone(vit_neck, text_encoder): return SAM3VLBackbone(visual=vit_neck, text=text_encoder, scalp=1) -def _create_transformer_encoder() -> TransformerEncoderFusion: +def _create_transformer_encoder( + num_feature_levels: int = 1, +) -> TransformerEncoderFusion: """Create transformer encoder with its layer.""" encoder_layer = TransformerEncoderLayer( activation="relu", @@ -144,7 +146,7 @@ def _create_transformer_encoder() -> TransformerEncoderFusion: layer=encoder_layer, num_layers=6, d_model=256, - num_feature_levels=1, + num_feature_levels=num_feature_levels, frozen=False, use_act_checkpoint=True, add_pooled_text_to_img_feat=False, @@ -296,6 +298,7 @@ def _create_sam3_model( dot_prod_scoring, inst_interactive_predictor, eval_mode, + num_feature_levels: int = 1, ): """Create the SAM3 image model.""" common_params = { @@ -303,7 +306,7 @@ def _create_sam3_model( "transformer": transformer, "input_geometry_encoder": input_geometry_encoder, "segmentation_head": segmentation_head, - "num_feature_levels": 1, + "num_feature_levels": num_feature_levels, "o2m_mask_predict": True, "dot_prod_scoring": dot_prod_scoring, "use_instance_query": False, @@ -515,9 +518,14 @@ def _create_vision_backbone( return vit_neck -def _create_sam3_transformer(has_presence_token: bool = True) -> TransformerWrapper: +def _create_sam3_transformer( + has_presence_token: bool = True, + num_feature_levels: int = 1, +) -> TransformerWrapper: """Create SAM3 transformer encoder and decoder.""" - encoder: TransformerEncoderFusion = _create_transformer_encoder() + encoder: TransformerEncoderFusion = _create_transformer_encoder( + num_feature_levels=num_feature_levels + ) decoder: TransformerDecoder = _create_transformer_decoder() return TransformerWrapper(encoder=encoder, decoder=decoder, d_model=256) @@ -566,6 +574,7 @@ def build_sam3_image_model( enable_segmentation=True, enable_inst_interactivity=False, compile=False, + num_feature_levels: int = 1, ): """ Build SAM3 image model @@ -600,7 +609,7 @@ def build_sam3_image_model( backbone = _create_vl_backbone(vision_encoder, text_encoder) # Create transformer components - transformer = _create_sam3_transformer() + transformer = _create_sam3_transformer(num_feature_levels=num_feature_levels) # Create dot product scoring dot_prod_scoring = _create_dot_product_scoring() @@ -628,6 +637,7 @@ def build_sam3_image_model( dot_prod_scoring, inst_predictor, eval_mode, + num_feature_levels=num_feature_levels, ) if load_from_HF and checkpoint_path is None: checkpoint_path = download_ckpt_from_hf() @@ -683,7 +693,9 @@ def build_sam3_video_model( visual_neck = _create_vision_backbone() text_encoder = _create_text_encoder(bpe_path) backbone = SAM3VLBackbone(scalp=1, visual=visual_neck, text=text_encoder) - transformer = _create_sam3_transformer(has_presence_token=has_presence_token) + transformer = _create_sam3_transformer( + has_presence_token=has_presence_token, num_feature_levels=1 + ) segmentation_head: UniversalSegmentationHead = _create_segmentation_head() input_geometry_encoder = _create_geometry_encoder() diff --git a/scripts/benchmark_sam3_artifacts.py b/scripts/benchmark_sam3_artifacts.py index 52381ec..7373cd0 100644 --- a/scripts/benchmark_sam3_artifacts.py +++ b/scripts/benchmark_sam3_artifacts.py @@ -205,6 +205,12 @@ def main() -> None: default=Path("artifacts/export"), help="Directory with exported artifacts", ) + parser.add_argument( + "--num-feature-levels", + type=int, + default=1, + help="Number of feature levels to use", + ) parser.add_argument("--warmup", type=int, default=3) parser.add_argument("--iters", type=int, default=10) args = parser.parse_args() @@ -214,7 +220,10 @@ def main() -> None: raise ValueError("Provide at least one prompt") model = build_sam3_image_model( - device=args.device, eval_mode=True, enable_segmentation=True + device=args.device, + eval_mode=True, + enable_segmentation=True, + num_feature_levels=args.num_feature_levels, ) model.eval() diff --git a/scripts/benchmark_sam3_export_times.py b/scripts/benchmark_sam3_export_times.py index 2b8bcf6..dafc649 100644 --- a/scripts/benchmark_sam3_export_times.py +++ b/scripts/benchmark_sam3_export_times.py @@ -59,17 +59,28 @@ def main() -> None: type=str, default="cuda" if torch.cuda.is_available() else "cpu", ) + parser.add_argument( + "--num-feature-levels", + type=int, + default=1, + help="Number of feature levels to use", + ) args = parser.parse_args() device = torch.device(args.device) model = build_sam3_image_model( - device=args.device, eval_mode=True, enable_segmentation=True + device=args.device, + eval_mode=True, + enable_segmentation=True, + num_feature_levels=args.num_feature_levels, ) model.eval() image = _prepare_image(_load_image(args.image, device), size=1008) inputs = _make_inputs(1, 1008, 1008, str(device), num_boxes=1) + decoder_inputs = None + decoder_inputs_error = None with torch.no_grad(): backbone_out = model.backbone.forward_image(image) text_encoder = model.backbone.language_backbone @@ -93,7 +104,10 @@ def main() -> None: device=img_feats.device, dtype=torch.bool, ) - decoder_inputs = _make_decoder_only_inputs(model, inputs) + try: + decoder_inputs = _make_decoder_only_inputs(model, inputs) + except Exception as exc: + decoder_inputs_error = exc def export_image_encoder(): _export_image_encoder(model, image) @@ -105,6 +119,14 @@ def export_encoder_fusion(): encoder_wrapper = ( EncoderFusionWrapper(model.transformer.encoder).to(img_feats.device).eval() ) + if args.num_feature_levels != 1: + raise RuntimeError( + "encoder_fusion export currently expects num_feature_levels=1" + ) + if args.num_feature_levels != 1: + raise RuntimeError( + "encoder_fusion export currently expects num_feature_levels=1" + ) torch.export.export( encoder_wrapper, (img_feats, img_pos, img_mask, text_memory, text_attention_mask), @@ -129,14 +151,25 @@ def export_full_pipeline(): _export_full_sam3_pipeline(model, inputs) def export_decoder_only(): + if decoder_inputs_error is not None: + raise decoder_inputs_error _export_decoder_only(model, decoder_inputs) with torch.no_grad(): _time("Export image encoder", export_image_encoder) _time("Export text encoder", export_text_encoder) - _time("Export encoder fusion", export_encoder_fusion) - _time("Export full pipeline", export_full_pipeline) - _time("Export decoder only", export_decoder_only) + if args.num_feature_levels == 1: + _time("Export encoder fusion", export_encoder_fusion) + else: + print("Export encoder fusion: skipped (num_feature_levels != 1)") + try: + _time("Export full pipeline", export_full_pipeline) + except Exception as exc: + print(f"Export full pipeline: failed ({type(exc).__name__}: {exc})") + try: + _time("Export decoder only", export_decoder_only) + except Exception as exc: + print(f"Export decoder only: failed ({type(exc).__name__}: {exc})") if __name__ == "__main__": diff --git a/scripts/benchmark_sam3_load_time.py b/scripts/benchmark_sam3_load_time.py index 77ea5f8..47f5586 100644 --- a/scripts/benchmark_sam3_load_time.py +++ b/scripts/benchmark_sam3_load_time.py @@ -23,6 +23,12 @@ def main() -> None: type=str, default="cuda" if torch.cuda.is_available() else "cpu", ) + parser.add_argument( + "--num-feature-levels", + type=int, + default=1, + help="Unused; kept for parity with other benchmarks", + ) args = parser.parse_args() if args.device.startswith("cuda"): diff --git a/scripts/export_sam3_full_pipeline.py b/scripts/export_sam3_full_pipeline.py index b9dc5c1..4c6d902 100644 --- a/scripts/export_sam3_full_pipeline.py +++ b/scripts/export_sam3_full_pipeline.py @@ -9,6 +9,8 @@ REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT)) +from typing import Any, cast + from sam3.model_builder import build_sam3_image_model from sam3.model.data_misc import FindStage from sam3.model.geometry_encoders import Prompt @@ -29,7 +31,7 @@ def forward( box_mask: torch.Tensor, box_labels: torch.Tensor, ): - model = self.model + model = cast(Any, self.model) backbone_out = model.backbone.forward_image(images) text_encoder = model.backbone.language_backbone _, text_tokens = text_encoder.encoder(token_ids) @@ -129,6 +131,12 @@ def main() -> None: type=str, default="cuda" if torch.cuda.is_available() else "cpu", ) + parser.add_argument( + "--num-feature-levels", + type=int, + default=1, + help="Number of feature levels to use", + ) parser.add_argument( "--out-dir", type=Path, @@ -142,7 +150,10 @@ def main() -> None: raise ValueError("Provide at least one prompt") model = build_sam3_image_model( - device=args.device, eval_mode=True, enable_segmentation=True + device=args.device, + eval_mode=True, + enable_segmentation=True, + num_feature_levels=args.num_feature_levels, ) model.eval() diff --git a/scripts/test_sam3_full_pipeline.py b/scripts/test_sam3_full_pipeline.py index b5db78c..6118e01 100644 --- a/scripts/test_sam3_full_pipeline.py +++ b/scripts/test_sam3_full_pipeline.py @@ -69,6 +69,12 @@ def main() -> None: type=str, default="cuda" if torch.cuda.is_available() else "cpu", ) + parser.add_argument( + "--num-feature-levels", + type=int, + default=1, + help="Number of feature levels to use", + ) parser.add_argument( "--artifact", type=Path, @@ -82,7 +88,10 @@ def main() -> None: raise ValueError("Provide at least one prompt") model = build_sam3_image_model( - device=args.device, eval_mode=True, enable_segmentation=True + device=args.device, + eval_mode=True, + enable_segmentation=True, + num_feature_levels=args.num_feature_levels, ) model.eval() From ad2c3382322b42381eb45d751e97a0fb8e880acb Mon Sep 17 00:00:00 2001 From: Ryan Avery Date: Fri, 6 Feb 2026 17:40:36 -0800 Subject: [PATCH 09/31] Tidy export timing benchmark output Remove redundant checks and keep feature-level guard output concise. --- scripts/benchmark_sam3_export_times.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/benchmark_sam3_export_times.py b/scripts/benchmark_sam3_export_times.py index dafc649..6ae5302 100644 --- a/scripts/benchmark_sam3_export_times.py +++ b/scripts/benchmark_sam3_export_times.py @@ -119,10 +119,6 @@ def export_encoder_fusion(): encoder_wrapper = ( EncoderFusionWrapper(model.transformer.encoder).to(img_feats.device).eval() ) - if args.num_feature_levels != 1: - raise RuntimeError( - "encoder_fusion export currently expects num_feature_levels=1" - ) if args.num_feature_levels != 1: raise RuntimeError( "encoder_fusion export currently expects num_feature_levels=1" From 8fd38fd8b22833c790d938c2ec2d2ae28745eb64 Mon Sep 17 00:00:00 2001 From: Ryan Avery Date: Fri, 6 Feb 2026 18:10:57 -0800 Subject: [PATCH 10/31] Clean export logs and mark export tests slow Remove tracked export stderr logs and export progress doc, ignore new logs, and mark export tests with the slow marker. --- .gitignore | 1 + docs/export_progress.md | 42 ------------------- ...amic_batch-export_dynamic_batch.stderr.txt | 0 ...08-1008_-export_dynamic_spatial.stderr.txt | 0 ...44-1344_-export_dynamic_spatial.stderr.txt | 0 ...ull_dynamic-export_full_dynamic.stderr.txt | 0 ...4-1344_-export_inference_shapes.stderr.txt | 0 ...8-1008_-export_inference_shapes.stderr.txt | 0 ...coder_export_loads-export_loads.stderr.txt | 0 ...port_matches_eager-export_match.stderr.txt | 0 ...der_export_static-export_static.stderr.txt | 0 ...amic_batch-export_dynamic_batch.stderr.txt | 0 ..._spatial-export_dynamic_spatial.stderr.txt | 0 ...l_16-16_-export_dynamic_spatial.stderr.txt | 0 ...l_36-36_-export_dynamic_spatial.stderr.txt | 0 ...l_72-72_-export_dynamic_spatial.stderr.txt | 0 ...6-16-4_-export_inference_shapes.stderr.txt | 0 ...es_1-4_-export_inference_shapes.stderr.txt | 0 ...6-36-8_-export_inference_shapes.stderr.txt | 0 ...es_2-8_-export_inference_shapes.stderr.txt | 0 ...coder_export_loads-export_loads.stderr.txt | 0 ...der_export_static-export_static.stderr.txt | 0 ...der_export_static-export_static.stderr.txt | 0 ...amic_batch-export_dynamic_batch.stderr.txt | 0 ..._len_16_-export_dynamic_seq_len.stderr.txt | 0 ..._len_32_-export_dynamic_seq_len.stderr.txt | 0 ...q_len_8_-export_dynamic_seq_len.stderr.txt | 0 ...s_1-32_-export_inference_shapes.stderr.txt | 0 ...es_1-8_-export_inference_shapes.stderr.txt | 0 ...s_2-16_-export_inference_shapes.stderr.txt | 0 ...s_2-32_-export_inference_shapes.stderr.txt | 0 ...s_3-32_-export_inference_shapes.stderr.txt | 0 ...coder_export_loads-export_loads.stderr.txt | 0 ...port_matches_eager-export_match.stderr.txt | 0 ...der_export_static-export_static.stderr.txt | 0 tests/export/test_decoder_export.py | 6 +++ tests/export/test_encoder_export.py | 4 ++ tests/export/test_image_encoder_export.py | 4 ++ tests/export/test_text_encoder_export.py | 4 ++ 39 files changed, 19 insertions(+), 42 deletions(-) delete mode 100644 docs/export_progress.md delete mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_batch-export_dynamic_batch.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_spatial_1008-1008_-export_dynamic_spatial.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_spatial_1344-1344_-export_dynamic_spatial.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_full_dynamic-export_full_dynamic.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_inference_shapes_1-1344-1344_-export_inference_shapes.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_inference_shapes_2-1008-1008_-export_inference_shapes.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_loads-export_loads.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_matches_eager-export_match.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_static-export_static.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_batch-export_dynamic_batch.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial-export_dynamic_spatial.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_16-16_-export_dynamic_spatial.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_36-36_-export_dynamic_spatial.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_72-72_-export_dynamic_spatial.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_1-16-16-4_-export_inference_shapes.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_1-4_-export_inference_shapes.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_2-36-36-8_-export_inference_shapes.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_2-8_-export_inference_shapes.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_loads-export_loads.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_static-export_static.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_image_encoder_export.py_test_image_encoder_export_static-export_static.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_batch-export_dynamic_batch.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_16_-export_dynamic_seq_len.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_32_-export_dynamic_seq_len.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_8_-export_dynamic_seq_len.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_1-32_-export_inference_shapes.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_1-8_-export_inference_shapes.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_2-16_-export_inference_shapes.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_2-32_-export_inference_shapes.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_3-32_-export_inference_shapes.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_loads-export_loads.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_matches_eager-export_match.stderr.txt delete mode 100644 tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_static-export_static.stderr.txt diff --git a/.gitignore b/.gitignore index 2a64259..e67edc1 100644 --- a/.gitignore +++ b/.gitignore @@ -132,6 +132,7 @@ weights/ checkpoints/ sam3_logs/ artifacts/ +tests/export/export_logs/ # Data files *.h5 diff --git a/docs/export_progress.md b/docs/export_progress.md deleted file mode 100644 index 1380faf..0000000 --- a/docs/export_progress.md +++ /dev/null @@ -1,42 +0,0 @@ -# Export Progress Summary - -## Current state - -- **Export scripts** now work end-to-end for the single-level path and produce PT2 - artifacts for image encoder, text encoder, encoder fusion, and decoder. -- **Test script** runs the exported artifacts, checks logits against eager, and - writes mask/box overlays. -- **Decoder export** is pinned to a **minimum batch of 2** to avoid export guard - failures. The test script pads inputs to batch >=2 when needed, then slices - outputs back to the original prompt count. - -## Changes made - -- `scripts/export_sam3_artifacts.py` - - Use single-level features (`backbone_fpn[-1]`) for encoder fusion export. - - Align image feature batch with prompt batch (repeat if needed). - - Use `torch.export.save(...)` for PT2 artifacts. - - Export decoder with a minimal prompt list (single prompt) and updated decoder - export logic to keep batch dynamic. - -- `scripts/test_sam3_artifacts.py` - - Use single-level features for encoder fusion. - - Repeat image features to match prompt batch for encoder fusion. - - Pad decoder inputs to batch >=2 if needed; compare against eager on the same - padded inputs and slice outputs back to the original prompt count. - - Resize masks to the input image for overlays and guard box drawing against - size mismatches. - -- `tests/export/test_decoder_export.py` - - Updated decoder export helper to attempt batch=1 (dynamic) and fall back to - batch=2. In practice, batch=2 is the stable path. - -- New script: `scripts/benchmark_sam3_artifacts.py` - - Benchmarks exported image encoder, text encoder, encoder fusion, decoder, and - compares to full eager inference time. - -## Open items - -- Decide whether to keep decoder export pinned to batch >=2 or revisit batch=1 - export constraints. -- Integrate the new benchmark script into docs or CI if desired. diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_batch-export_dynamic_batch.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_batch-export_dynamic_batch.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_spatial_1008-1008_-export_dynamic_spatial.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_spatial_1008-1008_-export_dynamic_spatial.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_spatial_1344-1344_-export_dynamic_spatial.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_dynamic_spatial_1344-1344_-export_dynamic_spatial.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_full_dynamic-export_full_dynamic.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_full_dynamic-export_full_dynamic.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_inference_shapes_1-1344-1344_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_inference_shapes_1-1344-1344_-export_inference_shapes.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_inference_shapes_2-1008-1008_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_inference_shapes_2-1008-1008_-export_inference_shapes.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_loads-export_loads.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_loads-export_loads.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_matches_eager-export_match.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_matches_eager-export_match.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_static-export_static.stderr.txt b/tests/export/export_logs/tests_export_test_decoder_export.py_test_decoder_export_static-export_static.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_batch-export_dynamic_batch.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_batch-export_dynamic_batch.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial-export_dynamic_spatial.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial-export_dynamic_spatial.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_16-16_-export_dynamic_spatial.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_16-16_-export_dynamic_spatial.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_36-36_-export_dynamic_spatial.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_36-36_-export_dynamic_spatial.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_72-72_-export_dynamic_spatial.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_dynamic_spatial_72-72_-export_dynamic_spatial.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_1-16-16-4_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_1-16-16-4_-export_inference_shapes.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_1-4_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_1-4_-export_inference_shapes.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_2-36-36-8_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_2-36-36-8_-export_inference_shapes.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_2-8_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_inference_shapes_2-8_-export_inference_shapes.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_loads-export_loads.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_loads-export_loads.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_static-export_static.stderr.txt b/tests/export/export_logs/tests_export_test_encoder_export.py_test_encoder_export_static-export_static.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_image_encoder_export.py_test_image_encoder_export_static-export_static.stderr.txt b/tests/export/export_logs/tests_export_test_image_encoder_export.py_test_image_encoder_export_static-export_static.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_batch-export_dynamic_batch.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_batch-export_dynamic_batch.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_16_-export_dynamic_seq_len.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_16_-export_dynamic_seq_len.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_32_-export_dynamic_seq_len.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_32_-export_dynamic_seq_len.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_8_-export_dynamic_seq_len.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_dynamic_seq_len_8_-export_dynamic_seq_len.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_1-32_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_1-32_-export_inference_shapes.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_1-8_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_1-8_-export_inference_shapes.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_2-16_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_2-16_-export_inference_shapes.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_2-32_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_2-32_-export_inference_shapes.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_3-32_-export_inference_shapes.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_inference_shapes_3-32_-export_inference_shapes.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_loads-export_loads.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_loads-export_loads.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_matches_eager-export_match.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_matches_eager-export_match.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_static-export_static.stderr.txt b/tests/export/export_logs/tests_export_test_text_encoder_export.py_test_text_encoder_export_static-export_static.stderr.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/export/test_decoder_export.py b/tests/export/test_decoder_export.py index ca769b5..b1d3ed8 100644 --- a/tests/export/test_decoder_export.py +++ b/tests/export/test_decoder_export.py @@ -304,6 +304,7 @@ def _export_decoder_only(model: Any, inputs): return exported +@pytest.mark.slow def test_decoder_export_static(sam3_model): device = get_device() inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) @@ -312,6 +313,7 @@ def test_decoder_export_static(sam3_model): assert exported is not None +@pytest.mark.slow def test_decoder_export_loads(sam3_model): device = get_device() inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) @@ -324,6 +326,7 @@ def test_decoder_export_loads(sam3_model): assert len(out) == 4 +@pytest.mark.slow def test_decoder_export_matches_eager(sam3_model): device = get_device() inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) @@ -342,6 +345,7 @@ def test_decoder_export_matches_eager(sam3_model): torch.testing.assert_close(eager, compiled, rtol=1e-3, atol=1e-3) +@pytest.mark.slow @pytest.mark.parametrize("batch", [1, 2]) def test_full_sam3_pipeline_export_inference_shapes(sam3_model, batch: int): device = get_device() @@ -355,6 +359,7 @@ def test_full_sam3_pipeline_export_inference_shapes(sam3_model, batch: int): assert isinstance(out, tuple) +@pytest.mark.slow def test_decoder_only_export_loads(sam3_model): device = get_device() inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) @@ -368,6 +373,7 @@ def test_decoder_only_export_loads(sam3_model): assert len(out) == 4 +@pytest.mark.slow def test_decoder_only_export_matches_eager(sam3_model): device = get_device() inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) diff --git a/tests/export/test_encoder_export.py b/tests/export/test_encoder_export.py index 65f7f08..3285628 100644 --- a/tests/export/test_encoder_export.py +++ b/tests/export/test_encoder_export.py @@ -99,6 +99,7 @@ def _export_encoder(model: torch.nn.Module, img_feats, img_pos, prompt, prompt_m return exported +@pytest.mark.slow def test_encoder_export_static(sam3_model): device = get_device() img_feats, img_pos, img_mask = _make_image_tokens(1, 72, 72, device) @@ -108,6 +109,7 @@ def test_encoder_export_static(sam3_model): assert exported is not None +@pytest.mark.slow def test_encoder_export_loads(sam3_model): device = get_device() img_feats, img_pos, img_mask = _make_image_tokens(1, 72, 72, device) @@ -121,6 +123,7 @@ def test_encoder_export_loads(sam3_model): assert len(out) == 6 +@pytest.mark.slow def test_encoder_export_matches_eager(sam3_model): device = get_device() img_feats, img_pos, img_mask = _make_image_tokens(1, 72, 72, device) @@ -137,6 +140,7 @@ def test_encoder_export_matches_eager(sam3_model): torch.testing.assert_close(eager, compiled, rtol=1e-3, atol=1e-3) +@pytest.mark.slow @pytest.mark.parametrize("batch,seq_len", [(1, 4), (2, 8)]) def test_encoder_export_inference_shapes(sam3_model, batch: int, seq_len: int): device = get_device() diff --git a/tests/export/test_image_encoder_export.py b/tests/export/test_image_encoder_export.py index b97281f..9597691 100644 --- a/tests/export/test_image_encoder_export.py +++ b/tests/export/test_image_encoder_export.py @@ -48,6 +48,7 @@ def _export_image_encoder(model: torch.nn.Module, images: torch.Tensor): return exported +@pytest.mark.slow def test_image_encoder_export_static(sam3_model): device = get_device() images = _make_images(1, 1008, 1008, device) @@ -56,6 +57,7 @@ def test_image_encoder_export_static(sam3_model): assert exported is not None +@pytest.mark.slow def test_image_encoder_export_loads(sam3_model): device = get_device() images = _make_images(1, 1008, 1008, device) @@ -68,6 +70,7 @@ def test_image_encoder_export_loads(sam3_model): assert len(out) == 3 +@pytest.mark.slow def test_image_encoder_export_matches_eager(sam3_model): device = get_device() images = _make_images(1, 1008, 1008, device) @@ -87,6 +90,7 @@ def test_image_encoder_export_matches_eager(sam3_model): torch.testing.assert_close(eager, compiled, rtol=1e-3, atol=1e-3) +@pytest.mark.slow @pytest.mark.parametrize("batch,height,width", [(2, 1008, 1008)]) def test_image_encoder_export_inference_shapes( sam3_model, batch: int, height: int, width: int diff --git a/tests/export/test_text_encoder_export.py b/tests/export/test_text_encoder_export.py index 83c14df..79ec249 100644 --- a/tests/export/test_text_encoder_export.py +++ b/tests/export/test_text_encoder_export.py @@ -55,6 +55,7 @@ def _export_text_encoder(model: Any, token_ids: torch.Tensor): return exported +@pytest.mark.slow def test_text_encoder_export_static(sam3_model): device = get_device() vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size @@ -64,6 +65,7 @@ def test_text_encoder_export_static(sam3_model): assert exported is not None +@pytest.mark.slow def test_text_encoder_export_loads(sam3_model): device = get_device() vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size @@ -77,6 +79,7 @@ def test_text_encoder_export_loads(sam3_model): assert len(out) == 2 +@pytest.mark.slow def test_text_encoder_export_matches_eager(sam3_model): device = get_device() vocab_size = sam3_model.backbone.language_backbone.encoder.vocab_size @@ -94,6 +97,7 @@ def test_text_encoder_export_matches_eager(sam3_model): torch.testing.assert_close(eager_out[1], export_out[1], rtol=1e-3, atol=1e-3) +@pytest.mark.slow @pytest.mark.parametrize("batch,seq_len", [(1, 32), (2, 32)]) def test_text_encoder_export_inference_shapes(sam3_model, batch: int, seq_len: int): device = get_device() From 35d756e2014bd2c93884f6f28ae5cdfda617e34c Mon Sep 17 00:00:00 2001 From: Ryan Avery Date: Fri, 6 Feb 2026 18:16:36 -0800 Subject: [PATCH 11/31] Register slow export tests Declare the slow pytest marker to avoid warnings for export benchmarks. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b8eb2c7..4bb315a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,3 +152,4 @@ testpaths = ["tests"] python_files = "test_*.py" python_classes = "Test*" python_functions = "test_*" +markers = ["slow: long-running export and artifact tests"] From 3a5fa59743d2f595d196709fefdf2678a4fd5051 Mon Sep 17 00:00:00 2001 From: Ryan Avery Date: Wed, 11 Feb 2026 17:00:20 -0800 Subject: [PATCH 12/31] Relax position encoding check for dynamic prompts Use 2-input full pipeline export in tests and scripts, log output shapes, and add a standalone 2-input export helper. This keeps prompt count dynamic for export artifacts while preserving benchmarks and artifact validation. --- sam3/model/position_encoding.py | 10 +- scripts/benchmark_sam3_artifacts.py | 97 +++++++------------ scripts/benchmark_sam3_export_times.py | 6 +- scripts/export_sam3_artifacts.py | 24 +---- scripts/export_sam3_full_pipeline.py | 67 ++++--------- scripts/test_sam3_artifacts.py | 124 ++++++++----------------- tests/export/test_decoder_export.py | 120 ++++++++++-------------- tests/export/utils.py | 24 +++++ 8 files changed, 170 insertions(+), 302 deletions(-) diff --git a/sam3/model/position_encoding.py b/sam3/model/position_encoding.py index 7419242..e112447 100644 --- a/sam3/model/position_encoding.py +++ b/sam3/model/position_encoding.py @@ -53,7 +53,7 @@ def __init__( def _encode_xy(self, x, y): # The positions are expected to be normalized - assert len(x) == len(y) and x.ndim == y.ndim == 1 + # torch._check(len(x) == len(y) and x.ndim == y.ndim == 1) x_embed = x * self.scale y_embed = y * self.scale @@ -62,12 +62,8 @@ def _encode_xy(self, x, y): pos_x = x_embed[:, None] / dim_t pos_y = y_embed[:, None] / dim_t - pos_x = torch.stack( - (pos_x[:, 0::2].sin(), pos_x[:, 1::2].cos()), dim=2 - ).flatten(1) - pos_y = torch.stack( - (pos_y[:, 0::2].sin(), pos_y[:, 1::2].cos()), dim=2 - ).flatten(1) + pos_x = torch.stack((pos_x[:, 0::2].sin(), pos_x[:, 1::2].cos()), dim=2).flatten(1) + pos_y = torch.stack((pos_y[:, 0::2].sin(), pos_y[:, 1::2].cos()), dim=2).flatten(1) return pos_x, pos_y @torch.no_grad() diff --git a/scripts/benchmark_sam3_artifacts.py b/scripts/benchmark_sam3_artifacts.py index 7373cd0..f0ea143 100644 --- a/scripts/benchmark_sam3_artifacts.py +++ b/scripts/benchmark_sam3_artifacts.py @@ -34,42 +34,31 @@ def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: def _make_inputs(model, image: torch.Tensor, prompts): device = image.device - num_prompts = len(prompts) - num_images = int(image.shape[0]) tokenizer = model.backbone.language_backbone.tokenizer token_ids = tokenizer(prompts, context_length=32).to(device) - img_ids = torch.arange(num_images, device=device, dtype=torch.long) - img_ids = img_ids.repeat_interleave(num_prompts) - text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) - text_ids = text_ids.repeat(num_images) - - box_embeddings = torch.zeros(1, num_prompts, 4, device=device) - box_mask = torch.zeros(num_prompts, 1, device=device, dtype=torch.bool) - box_labels = torch.zeros(1, num_prompts, device=device, dtype=torch.long) - return ( image, token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, ) def _run_full_model(model, inputs): - ( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, - ) = inputs + images, token_ids = inputs + num_images = images.shape[0] + num_prompts = token_ids.shape[0] + device = images.device + bs = num_images * num_prompts + + img_ids = torch.arange(num_images, device=device, dtype=torch.long) + img_ids = img_ids.repeat_interleave(num_prompts) + text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) + text_ids = text_ids.repeat(num_images) + + box_embeddings = torch.zeros(1, bs, 4, device=device) + box_mask = torch.zeros(bs, 1, device=device, dtype=torch.bool) + box_labels = torch.zeros(1, bs, device=device, dtype=torch.long) backbone_out = model.backbone.forward_image(images) text_encoder = model.backbone.language_backbone _, text_tokens = text_encoder.encoder(token_ids) @@ -113,15 +102,20 @@ def _make_decoder_only_inputs_from_model( text_attention_mask, inputs, ): - ( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, - ) = inputs + images, token_ids = inputs + num_images = images.shape[0] + num_prompts = token_ids.shape[0] + device = images.device + bs = num_images * num_prompts + + img_ids = torch.arange(num_images, device=device, dtype=torch.long) + img_ids = img_ids.repeat_interleave(num_prompts) + text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) + text_ids = text_ids.repeat(num_images) + + box_embeddings = torch.zeros(1, bs, 4, device=device) + box_mask = torch.zeros(bs, 1, device=device, dtype=torch.bool) + box_labels = torch.zeros(1, bs, device=device, dtype=torch.long) backbone_out = { "backbone_fpn": backbone_fpn, "vision_pos_enc": vision_pos_enc, @@ -147,9 +141,7 @@ def _make_decoder_only_inputs_from_model( prompt, prompt_mask, backbone_out = model._encode_prompt( backbone_out, find_input, geometric_prompt ) - backbone_out, encoder_out, _ = model._run_encoder( - backbone_out, find_input, prompt, prompt_mask - ) + backbone_out, encoder_out, _ = model._run_encoder(backbone_out, find_input, prompt, prompt_mask) return ( backbone_out["backbone_fpn"], img_ids, @@ -286,32 +278,7 @@ def encoder_from_outputs(image_out, text_out): def encoder_fn(): encoder_from_outputs(cached_image_out, cached_text_out) - ( - pipeline_images, - pipeline_token_ids, - pipeline_img_ids, - pipeline_text_ids, - pipeline_box_embeddings, - pipeline_box_mask, - pipeline_box_labels, - ) = inputs - if pipeline_token_ids.shape[0] < 2: - repeat = 2 // pipeline_token_ids.shape[0] - pipeline_token_ids = pipeline_token_ids.repeat(repeat, 1) - pipeline_img_ids = pipeline_img_ids.repeat(repeat) - pipeline_text_ids = pipeline_text_ids.repeat(repeat) - pipeline_box_embeddings = pipeline_box_embeddings.repeat(1, repeat, 1) - pipeline_box_mask = pipeline_box_mask.repeat(repeat, 1) - pipeline_box_labels = pipeline_box_labels.repeat(1, repeat) - pipeline_inputs = ( - pipeline_images, - pipeline_token_ids, - pipeline_img_ids, - pipeline_text_ids, - pipeline_box_embeddings, - pipeline_box_mask, - pipeline_box_labels, - ) + pipeline_inputs = inputs def pipeline_fn(): pipeline_module(*pipeline_inputs) @@ -346,9 +313,7 @@ def pipeline_fn(): decoder_prompt = decoder_prompt.repeat(1, repeat, 1) decoder_prompt_mask = decoder_prompt_mask.repeat(repeat, 1) decoder_valid_ratios = decoder_valid_ratios.repeat(repeat, 1, 1) - decoder_backbone_fpn = [ - feat.repeat(repeat, 1, 1, 1) for feat in decoder_backbone_fpn - ] + decoder_backbone_fpn = [feat.repeat(repeat, 1, 1, 1) for feat in decoder_backbone_fpn] decoder_only_inputs = ( decoder_backbone_fpn, decoder_img_ids, diff --git a/scripts/benchmark_sam3_export_times.py b/scripts/benchmark_sam3_export_times.py index 6ae5302..b45b6a8 100644 --- a/scripts/benchmark_sam3_export_times.py +++ b/scripts/benchmark_sam3_export_times.py @@ -77,7 +77,7 @@ def main() -> None: model.eval() image = _prepare_image(_load_image(args.image, device), size=1008) - inputs = _make_inputs(1, 1008, 1008, str(device), num_boxes=1) + inputs = _make_inputs(1, 1008, 1008, str(device)) decoder_inputs = None decoder_inputs_error = None @@ -120,9 +120,7 @@ def export_encoder_fusion(): EncoderFusionWrapper(model.transformer.encoder).to(img_feats.device).eval() ) if args.num_feature_levels != 1: - raise RuntimeError( - "encoder_fusion export currently expects num_feature_levels=1" - ) + raise RuntimeError("encoder_fusion export currently expects num_feature_levels=1") torch.export.export( encoder_wrapper, (img_feats, img_pos, img_mask, text_memory, text_attention_mask), diff --git a/scripts/export_sam3_artifacts.py b/scripts/export_sam3_artifacts.py index 8f25647..7cc0426 100644 --- a/scripts/export_sam3_artifacts.py +++ b/scripts/export_sam3_artifacts.py @@ -39,29 +39,13 @@ def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: def _make_inputs(model, image: torch.Tensor, prompts): device = image.device - num_prompts = len(prompts) - num_images = int(image.shape[0]) tokenizer = model.backbone.language_backbone.tokenizer token_ids = tokenizer(prompts, context_length=32).to(device) - img_ids = torch.arange(num_images, device=device, dtype=torch.long) - img_ids = img_ids.repeat_interleave(num_prompts) - text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) - text_ids = text_ids.repeat(num_images) - - box_embeddings = torch.zeros(1, num_prompts, 4, device=device) - box_mask = torch.zeros(num_prompts, 1, device=device, dtype=torch.bool) - box_labels = torch.zeros(1, num_prompts, device=device, dtype=torch.long) - return ( image, token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, ) @@ -101,9 +85,7 @@ def main() -> None: if not prompts: raise ValueError("Provide at least one prompt") - model = build_sam3_image_model( - device=args.device, eval_mode=True, enable_segmentation=True - ) + model = build_sam3_image_model(device=args.device, eval_mode=True, enable_segmentation=True) model.eval() image = _load_image(args.image, torch.device(args.device)) @@ -139,9 +121,7 @@ def main() -> None: img_pos = img_pos.repeat(prompt_batch, 1, 1, 1) img_mask = img_mask.repeat(prompt_batch, 1, 1) - encoder_wrapper = ( - EncoderFusionWrapper(model.transformer.encoder).to(img_feats.device).eval() - ) + encoder_wrapper = EncoderFusionWrapper(model.transformer.encoder).to(img_feats.device).eval() encoder = torch.export.export( encoder_wrapper, (img_feats, img_pos, img_mask, prompt, prompt_mask), diff --git a/scripts/export_sam3_full_pipeline.py b/scripts/export_sam3_full_pipeline.py index 4c6d902..9f50f3e 100644 --- a/scripts/export_sam3_full_pipeline.py +++ b/scripts/export_sam3_full_pipeline.py @@ -25,13 +25,22 @@ def forward( self, images: torch.Tensor, token_ids: torch.Tensor, - img_ids: torch.Tensor, - text_ids: torch.Tensor, - box_embeddings: torch.Tensor, - box_mask: torch.Tensor, - box_labels: torch.Tensor, ): model = cast(Any, self.model) + num_images = images.shape[0] + num_prompts = token_ids.shape[0] + device = images.device + bs = num_images * num_prompts + + img_ids = torch.arange(num_images, device=device, dtype=torch.long) + img_ids = img_ids.repeat_interleave(num_prompts) + text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) + text_ids = text_ids.repeat(num_images) + + box_embeddings = torch.zeros(1, bs, 4, device=device) + box_mask = torch.zeros(bs, 1, device=device, dtype=torch.bool) + box_labels = torch.zeros(1, bs, device=device, dtype=torch.long) + backbone_out = model.backbone.forward_image(images) text_encoder = model.backbone.language_backbone _, text_tokens = text_encoder.encoder(token_ids) @@ -48,12 +57,8 @@ def forward( input_boxes=box_embeddings, input_boxes_mask=box_mask, input_boxes_label=box_labels, - input_points=torch.zeros( - 0, int(token_ids.shape[0]), 2, device=images.device - ), - input_points_mask=torch.zeros( - int(token_ids.shape[0]), 0, device=images.device, dtype=torch.bool - ), + input_points=torch.zeros(0, bs, 2, device=device), + input_points_mask=torch.zeros(bs, 0, device=device, dtype=torch.bool), ) geometric_prompt = Prompt( box_embeddings=box_embeddings, @@ -92,23 +97,10 @@ def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: def _make_inputs(model, image: torch.Tensor, prompts): device = image.device - num_prompts = len(prompts) - num_images = int(image.shape[0]) - token_ids = model.backbone.language_backbone.tokenizer( - prompts, context_length=32 - ).to(device) - img_ids = torch.arange(num_images, device=device, dtype=torch.long) - img_ids = img_ids.repeat_interleave(num_prompts) - text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) - text_ids = text_ids.repeat(num_images) + token_ids = model.backbone.language_backbone.tokenizer(prompts, context_length=32).to(device) return ( image, token_ids, - img_ids, - text_ids, - torch.zeros(1, num_prompts, 4, device=device), - torch.zeros(num_prompts, 1, device=device, dtype=torch.bool), - torch.zeros(1, num_prompts, device=device, dtype=torch.long), ) @@ -157,9 +149,7 @@ def main() -> None: ) model.eval() - image = _prepare_image( - _load_image(args.image, torch.device(args.device)), size=1008 - ) + image = _prepare_image(_load_image(args.image, torch.device(args.device)), size=1008) inputs = _make_inputs(model, image, prompts) wrapper = FullSam3PipelineWrapper(model).to(image.device).eval() if image.shape[0] < 2: @@ -167,11 +157,6 @@ def main() -> None: export_inputs = ( image.repeat(repeat, 1, 1, 1), inputs[1].repeat(repeat, 1), - inputs[2].repeat(repeat), - inputs[3].repeat(repeat), - inputs[4].repeat(1, repeat, 1), - inputs[5].repeat(repeat, 1), - inputs[6].repeat(1, repeat), ) else: export_inputs = inputs @@ -186,23 +171,9 @@ def main() -> None: 3: 1008, }, "token_ids": { - 0: torch.export.Dim.AUTO, + 0: torch.export.Dim("num_prompts", min=1), 1: 32, }, - "img_ids": {0: torch.export.Dim.AUTO}, - "text_ids": {0: torch.export.Dim.AUTO}, - "box_embeddings": { - 0: 1, - 1: torch.export.Dim.AUTO, - }, - "box_mask": { - 0: torch.export.Dim.AUTO, - 1: 1, - }, - "box_labels": { - 0: 1, - 1: torch.export.Dim.AUTO, - }, }, strict=False, prefer_deferred_runtime_asserts_over_guards=True, diff --git a/scripts/test_sam3_artifacts.py b/scripts/test_sam3_artifacts.py index dd8348b..5d6c678 100644 --- a/scripts/test_sam3_artifacts.py +++ b/scripts/test_sam3_artifacts.py @@ -41,42 +41,31 @@ def _prepare_image(image: torch.Tensor, size: int) -> torch.Tensor: def _make_inputs(model, image: torch.Tensor, prompts): device = image.device - num_prompts = len(prompts) - num_images = int(image.shape[0]) tokenizer = model.backbone.language_backbone.tokenizer token_ids = tokenizer(prompts, context_length=32).to(device) - img_ids = torch.arange(num_images, device=device, dtype=torch.long) - img_ids = img_ids.repeat_interleave(num_prompts) - text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) - text_ids = text_ids.repeat(num_images) - - box_embeddings = torch.zeros(1, num_prompts, 4, device=device) - box_mask = torch.zeros(num_prompts, 1, device=device, dtype=torch.bool) - box_labels = torch.zeros(1, num_prompts, device=device, dtype=torch.long) - return ( image, token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, ) def _run_full_model(model, inputs): - ( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, - ) = inputs + images, token_ids = inputs + num_images = images.shape[0] + num_prompts = token_ids.shape[0] + device = images.device + bs = num_images * num_prompts + + img_ids = torch.arange(num_images, device=device, dtype=torch.long) + img_ids = img_ids.repeat_interleave(num_prompts) + text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) + text_ids = text_ids.repeat(num_images) + + box_embeddings = torch.zeros(1, bs, 4, device=device) + box_mask = torch.zeros(bs, 1, device=device, dtype=torch.bool) + box_labels = torch.zeros(1, bs, device=device, dtype=torch.long) backbone_out = model.backbone.forward_image(images) text_encoder = model.backbone.language_backbone _, text_tokens = text_encoder.encoder(token_ids) @@ -125,15 +114,20 @@ def _make_decoder_only_inputs_from_model( text_attention_mask, inputs, ): - ( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, - ) = inputs + images, token_ids = inputs + num_images = images.shape[0] + num_prompts = token_ids.shape[0] + device = images.device + bs = num_images * num_prompts + + img_ids = torch.arange(num_images, device=device, dtype=torch.long) + img_ids = img_ids.repeat_interleave(num_prompts) + text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) + text_ids = text_ids.repeat(num_images) + + box_embeddings = torch.zeros(1, bs, 4, device=device) + box_mask = torch.zeros(bs, 1, device=device, dtype=torch.bool) + box_labels = torch.zeros(1, bs, device=device, dtype=torch.long) backbone_out = { "backbone_fpn": backbone_fpn, "vision_pos_enc": vision_pos_enc, @@ -159,9 +153,7 @@ def _make_decoder_only_inputs_from_model( prompt, prompt_mask, backbone_out = model._encode_prompt( backbone_out, find_input, geometric_prompt ) - backbone_out, encoder_out, _ = model._run_encoder( - backbone_out, find_input, prompt, prompt_mask - ) + backbone_out, encoder_out, _ = model._run_encoder(backbone_out, find_input, prompt, prompt_mask) return ( backbone_out["backbone_fpn"], img_ids, @@ -198,9 +190,7 @@ def _color_palette(num_colors: int): return [base[i % len(base)] for i in range(num_colors)] -def _overlay_masks( - image: Image.Image, masks: torch.Tensor, scores: torch.Tensor, out_path: Path -): +def _overlay_masks(image: Image.Image, masks: torch.Tensor, scores: torch.Tensor, out_path: Path): num_prompts, num_queries = scores.shape[:2] best_idx = scores.squeeze(-1).argmax(dim=1) colors = _color_palette(num_prompts) @@ -219,9 +209,7 @@ def _overlay_masks( blended.convert("RGB").save(out_path) -def _draw_boxes( - image: Image.Image, boxes_xyxy: torch.Tensor, scores: torch.Tensor, out_path: Path -): +def _draw_boxes(image: Image.Image, boxes_xyxy: torch.Tensor, scores: torch.Tensor, out_path: Path): num_prompts, num_queries = scores.shape[:2] best_idx = scores.squeeze(-1).argmax(dim=1).clamp(max=boxes_xyxy.shape[1] - 1) colors = _color_palette(num_prompts) @@ -282,9 +270,7 @@ def main() -> None: raise ValueError("Provide at least one prompt") prompt_count = len(prompts) - model = build_sam3_image_model( - device=args.device, eval_mode=True, enable_segmentation=True - ) + model = build_sam3_image_model(device=args.device, eval_mode=True, enable_segmentation=True) model.eval() pil_image = _load_pil_image(args.image) @@ -293,9 +279,7 @@ def main() -> None: inputs = _make_inputs(model, image, prompts) with torch.no_grad(): - eager_masks, eager_boxes, eager_logits, eager_boxes_xyxy = _run_full_model( - model, inputs - ) + eager_masks, eager_boxes, eager_logits, eager_boxes_xyxy = _run_full_model(model, inputs) image_module = _load_export(args.artifact_dir / "image_encoder.pt2") text_module = _load_export(args.artifact_dir / "text_encoder.pt2") @@ -321,38 +305,10 @@ def main() -> None: device=img_feats.device, dtype=torch.bool, ) - enc_out = encoder_module( - img_feats, img_pos, img_mask, text_memory, text_attention_mask - ) + enc_out = encoder_module(img_feats, img_pos, img_mask, text_memory, text_attention_mask) assert isinstance(enc_out, tuple) - ( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, - ) = inputs - if token_ids.shape[0] < 2: - repeat = 2 // token_ids.shape[0] - token_ids = token_ids.repeat(repeat, 1) - img_ids = img_ids.repeat(repeat) - text_ids = text_ids.repeat(repeat) - box_embeddings = box_embeddings.repeat(1, repeat, 1) - box_mask = box_mask.repeat(repeat, 1) - box_labels = box_labels.repeat(1, repeat) - decoder_inputs = ( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, - ) - pipeline_logits, pipeline_boxes, pipeline_masks, pipeline_boxes_xyxy = ( - pipeline_module(*decoder_inputs) + pipeline_logits, pipeline_boxes, pipeline_masks, pipeline_boxes_xyxy = pipeline_module( + *inputs ) ( decoder_backbone_fpn, @@ -381,9 +337,7 @@ def main() -> None: decoder_prompt = decoder_prompt.repeat(1, repeat, 1) decoder_prompt_mask = decoder_prompt_mask.repeat(repeat, 1) decoder_valid_ratios = decoder_valid_ratios.repeat(repeat, 1, 1) - decoder_backbone_fpn = [ - feat.repeat(repeat, 1, 1, 1) for feat in decoder_backbone_fpn - ] + decoder_backbone_fpn = [feat.repeat(repeat, 1, 1, 1) for feat in decoder_backbone_fpn] pred_logits, pred_boxes, pred_masks, pred_boxes_xyxy = decoder_module( decoder_backbone_fpn, decoder_img_ids, @@ -395,8 +349,8 @@ def main() -> None: decoder_spatial_shapes, decoder_valid_ratios, ) - eager_ref_masks, eager_ref_boxes, eager_ref_logits, eager_ref_boxes_xyxy = ( - _run_full_model(model, decoder_inputs) + eager_ref_masks, eager_ref_boxes, eager_ref_logits, eager_ref_boxes_xyxy = _run_full_model( + model, inputs ) pred_logits = pred_logits[:prompt_count] diff --git a/tests/export/test_decoder_export.py b/tests/export/test_decoder_export.py index b1d3ed8..998cf6f 100644 --- a/tests/export/test_decoder_export.py +++ b/tests/export/test_decoder_export.py @@ -7,7 +7,7 @@ from sam3.model.data_misc import FindStage from sam3.model.geometry_encoders import Prompt -from tests.export.utils import capture_stderr_on_fail, get_device +from tests.export.utils import capture_stderr_on_fail, get_device, save_output_shapes class FullSam3PipelineWrapper(torch.nn.Module): @@ -19,13 +19,22 @@ def forward( self, images: torch.Tensor, token_ids: torch.Tensor, - img_ids: torch.Tensor, - text_ids: torch.Tensor, - box_embeddings: torch.Tensor, - box_mask: torch.Tensor, - box_labels: torch.Tensor, ): model = cast(Any, self.model) + num_images = images.shape[0] + num_prompts = token_ids.shape[0] + device = images.device + bs = num_images * num_prompts + + img_ids = torch.arange(num_images, device=device, dtype=torch.long) + img_ids = img_ids.repeat_interleave(num_prompts) + text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) + text_ids = text_ids.repeat(num_images) + + box_embeddings = torch.zeros(1, bs, 4, device=device) + box_mask = torch.zeros(bs, 1, device=device, dtype=torch.bool) + box_labels = torch.zeros(1, bs, device=device, dtype=torch.long) + backbone_out = model.backbone.forward_image(images) text_encoder = model.backbone.language_backbone _, text_tokens = text_encoder.encoder(token_ids) @@ -42,12 +51,8 @@ def forward( input_boxes=box_embeddings, input_boxes_mask=box_mask, input_boxes_label=box_labels, - input_points=torch.zeros( - 0, int(token_ids.shape[0]), 2, device=images.device - ), - input_points_mask=torch.zeros( - int(token_ids.shape[0]), 0, device=images.device, dtype=torch.bool - ), + input_points=torch.zeros(0, bs, 2, device=device), + input_points_mask=torch.zeros(bs, 0, device=device, dtype=torch.bool), ) geometric_prompt = Prompt( @@ -71,32 +76,31 @@ def forward( ) -def _make_inputs(batch: int, height: int, width: int, device: str, num_boxes: int = 0): +def _make_inputs(batch: int, height: int, width: int, device: str): images = torch.randn(batch, 3, height, width, device=device) token_ids = torch.ones(batch, 32, device=device, dtype=torch.long) token_ids[:, -1] = 0 - img_ids = torch.arange(batch, device=device, dtype=torch.long) - text_ids = torch.zeros(batch, device=device, dtype=torch.long) - box_embeddings = torch.rand(num_boxes, batch, 4, device=device) - if num_boxes > 0: - box_embeddings[..., 2:] = box_embeddings[..., 2:] * 0.5 - box_mask = torch.zeros(batch, num_boxes, device=device, dtype=torch.bool) - box_labels = torch.zeros(num_boxes, batch, device=device, dtype=torch.long) - return images, token_ids, img_ids, text_ids, box_embeddings, box_mask, box_labels + return images, token_ids def _make_decoder_only_inputs(model: Any, inputs): - ( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, - ) = inputs + images, token_ids = inputs model_any = cast(Any, model) model_any = model_any.eval() + num_images = images.shape[0] + num_prompts = token_ids.shape[0] + device = images.device + bs = num_images * num_prompts + + img_ids = torch.arange(num_images, device=device, dtype=torch.long) + img_ids = img_ids.repeat_interleave(num_prompts) + text_ids = torch.arange(num_prompts, device=device, dtype=torch.long) + text_ids = text_ids.repeat(num_images) + + box_embeddings = torch.zeros(1, bs, 4, device=device) + box_mask = torch.zeros(bs, 1, device=device, dtype=torch.bool) + box_labels = torch.zeros(1, bs, device=device, dtype=torch.long) + backbone_out = model_any.backbone.forward_image(images) text_encoder = model_any.backbone.language_backbone _, text_tokens = text_encoder.encoder(token_ids) @@ -113,10 +117,8 @@ def _make_decoder_only_inputs(model: Any, inputs): input_boxes=box_embeddings, input_boxes_mask=box_mask, input_boxes_label=box_labels, - input_points=torch.zeros(0, int(token_ids.shape[0]), 2, device=images.device), - input_points_mask=torch.zeros( - int(token_ids.shape[0]), 0, device=images.device, dtype=torch.bool - ), + input_points=torch.zeros(0, bs, 2, device=device), + input_points_mask=torch.zeros(bs, 0, device=device, dtype=torch.bool), ) geometric_prompt = Prompt( box_embeddings=box_embeddings, @@ -144,15 +146,7 @@ def _make_decoder_only_inputs(model: Any, inputs): def _export_full_sam3_pipeline(model: Any, inputs): - ( - images, - token_ids, - img_ids, - text_ids, - box_embeddings, - box_mask, - box_labels, - ) = inputs + images, token_ids = inputs device = images.device wrapper = FullSam3PipelineWrapper(model).to(device).eval() # type: ignore[arg-type] if images.shape[0] < 2: @@ -160,11 +154,6 @@ def _export_full_sam3_pipeline(model: Any, inputs): export_inputs = ( images.repeat(repeat, 1, 1, 1), token_ids.repeat(repeat, 1), - img_ids.repeat(repeat), - text_ids.repeat(repeat), - box_embeddings.repeat(1, repeat, 1), - box_mask.repeat(repeat, 1), - box_labels.repeat(1, repeat), ) else: export_inputs = inputs @@ -179,23 +168,9 @@ def _export_full_sam3_pipeline(model: Any, inputs): 3: 1008, }, "token_ids": { - 0: torch.export.Dim.AUTO, + 0: torch.export.Dim("num_prompts", min=1), 1: 32, }, - "img_ids": {0: torch.export.Dim.AUTO}, - "text_ids": {0: torch.export.Dim.AUTO}, - "box_embeddings": { - 0: 1, - 1: torch.export.Dim.AUTO, - }, - "box_mask": { - 0: torch.export.Dim.AUTO, - 1: 1, - }, - "box_labels": { - 0: 1, - 1: torch.export.Dim.AUTO, - }, }, strict=False, prefer_deferred_runtime_asserts_over_guards=True, @@ -307,7 +282,7 @@ def _export_decoder_only(model: Any, inputs): @pytest.mark.slow def test_decoder_export_static(sam3_model): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) + inputs = _make_inputs(1, 1008, 1008, device) with capture_stderr_on_fail("export_static"): exported = _export_full_sam3_pipeline(sam3_model, inputs) assert exported is not None @@ -316,7 +291,7 @@ def test_decoder_export_static(sam3_model): @pytest.mark.slow def test_decoder_export_loads(sam3_model): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) + inputs = _make_inputs(1, 1008, 1008, device) with capture_stderr_on_fail("export_loads"): exported = _export_full_sam3_pipeline(sam3_model, inputs) module = exported.module() @@ -329,7 +304,7 @@ def test_decoder_export_loads(sam3_model): @pytest.mark.slow def test_decoder_export_matches_eager(sam3_model): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) + inputs = _make_inputs(1, 1008, 1008, device) wrapper = FullSam3PipelineWrapper(sam3_model).to(device).eval() with torch.no_grad(): eager_out = wrapper(*inputs) @@ -338,6 +313,8 @@ def test_decoder_export_matches_eager(sam3_model): module = exported.module() with torch.no_grad(): export_out = module(*inputs) + save_output_shapes("full_pipeline_eager", inputs, eager_out) + save_output_shapes("full_pipeline_export", inputs, export_out) for eager, compiled in zip(eager_out, export_out): if eager is None: assert compiled is None @@ -349,20 +326,21 @@ def test_decoder_export_matches_eager(sam3_model): @pytest.mark.parametrize("batch", [1, 2]) def test_full_sam3_pipeline_export_inference_shapes(sam3_model, batch: int): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) + inputs = _make_inputs(1, 1008, 1008, device) with capture_stderr_on_fail("export_inference_shapes"): exported = _export_full_sam3_pipeline(sam3_model, inputs) module = exported.module() - new_inputs = _make_inputs(batch, 1008, 1008, device, num_boxes=1) + new_inputs = _make_inputs(batch, 1008, 1008, device) with torch.no_grad(): out = module(*new_inputs) + save_output_shapes(f"full_pipeline_export_batch_{batch}", new_inputs, out) assert isinstance(out, tuple) @pytest.mark.slow def test_decoder_only_export_loads(sam3_model): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) + inputs = _make_inputs(1, 1008, 1008, device) decoder_inputs = _make_decoder_only_inputs(sam3_model, inputs) with capture_stderr_on_fail("export_decoder_only_loads"): exported = _export_decoder_only(sam3_model, decoder_inputs) @@ -376,7 +354,7 @@ def test_decoder_only_export_loads(sam3_model): @pytest.mark.slow def test_decoder_only_export_matches_eager(sam3_model): device = get_device() - inputs = _make_inputs(1, 1008, 1008, device, num_boxes=1) + inputs = _make_inputs(1, 1008, 1008, device) decoder_inputs = _make_decoder_only_inputs(sam3_model, inputs) wrapper = DecoderOnlyWrapper(sam3_model).to(device).eval() with torch.no_grad(): @@ -386,5 +364,7 @@ def test_decoder_only_export_matches_eager(sam3_model): module = exported.module() with torch.no_grad(): export_out = module(*decoder_inputs) + save_output_shapes("decoder_only_eager", None, eager_out) + save_output_shapes("decoder_only_export", None, export_out) for eager, compiled in zip(eager_out, export_out): torch.testing.assert_close(eager, compiled, rtol=1e-3, atol=1e-3) diff --git a/tests/export/utils.py b/tests/export/utils.py index 8bff9a3..bddf172 100644 --- a/tests/export/utils.py +++ b/tests/export/utils.py @@ -41,3 +41,27 @@ def get_device() -> str: if device is None: device = "cuda" if torch.cuda.is_available() and not force_cpu else "cpu" return device + + +def save_output_shapes( + suffix: str, + inputs: tuple[torch.Tensor, ...] | None, + outputs: tuple[torch.Tensor | None, ...], +) -> None: + LOG_DIR.mkdir(parents=True, exist_ok=True) + lines: list[str] = [] + if inputs is not None: + for idx, value in enumerate(inputs): + lines.append( + f"input[{idx}] shape={tuple(value.shape)} dtype={value.dtype} device={value.device}" + ) + for idx, value in enumerate(outputs): + if value is None: + lines.append(f"output[{idx}] None") + else: + lines.append( + f"output[{idx}] shape={tuple(value.shape)} dtype={value.dtype} device={value.device}" + ) + log_path = LOG_DIR / f"{_current_test_name()}-{suffix}.shapes.txt" + with log_path.open("w", encoding="utf-8") as handle: + handle.write("\n".join(lines)) From f0399e7fce242a6b777286124b0c41cee91c28f6 Mon Sep 17 00:00:00 2001 From: Arpit Kalla Date: Tue, 17 Feb 2026 12:46:36 -0800 Subject: [PATCH 13/31] Fix compilation of position encoding Summary: When running with compile=True, we were getting the following error: P2186079344 Swapping to not use cudagraphs for a quik fix. Will follow up with swapping the Position embedding impl to not cache. Reviewed By: raedle Differential Revision: D93508585 fbshipit-source-id: 0eaa29fcf826611b6a652811a6f2472399014b84 --- sam3/model/sam3_video_inference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sam3/model/sam3_video_inference.py b/sam3/model/sam3_video_inference.py index 6f031be..18cd387 100644 --- a/sam3/model/sam3_video_inference.py +++ b/sam3/model/sam3_video_inference.py @@ -627,7 +627,7 @@ def _compile_model(self): ## Compile Tracker model components self.tracker.maskmem_backbone.forward = compile_wrapper( self.tracker.maskmem_backbone.forward, - mode="max-autotune", + mode="max-autotune-no-cudagraphs", fullgraph=True, dynamic=False, ) From 2d08d7312050b6f65b28f1b458cd91f8cd57814a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20R=C3=A4dle?= Date: Fri, 20 Feb 2026 14:51:26 -0800 Subject: [PATCH 14/31] Introduce torch compile option Summary: TSIA! Reviewed By: arpitkalla Differential Revision: D93826755 fbshipit-source-id: 0b7406b738590c4ee27099b1b1ce24b9c88680c5 --- sam3/model/sam3_video_predictor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sam3/model/sam3_video_predictor.py b/sam3/model/sam3_video_predictor.py index 13b1448..ce3b801 100644 --- a/sam3/model/sam3_video_predictor.py +++ b/sam3/model/sam3_video_predictor.py @@ -35,6 +35,7 @@ def __init__( async_loading_frames=False, video_loader_type="cv2", apply_temporal_disambiguation: bool = True, + compile: bool = False, ): self.async_loading_frames = async_loading_frames self.video_loader_type = video_loader_type @@ -48,6 +49,7 @@ def __init__( geo_encoder_use_img_cross_attn=geo_encoder_use_img_cross_attn, strict_state_dict_loading=strict_state_dict_loading, apply_temporal_disambiguation=apply_temporal_disambiguation, + compile=compile, ) .cuda() .eval() From f6e51f59500a87c576c2df2323ce56b9fd7a12de Mon Sep 17 00:00:00 2001 From: Haoran Wang Date: Tue, 24 Feb 2026 10:28:32 -0800 Subject: [PATCH 15/31] Add empty-frames guard to cv2 video loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: ## Context When cv2.VideoCapture opens a video but cannot decode any frames (e.g., due to an unsupported codec), the frames list is empty and np.stack crashes with "need at least one array to stack" — an unhelpful error message that obscures the real problem. ## This Diff - Add explicit check for empty frames list after cap.release() and before np.stack - Raise RuntimeError with descriptive message about possible causes (corrupted file, empty video, or unsupported codec) Differential Revision: D94142839 fbshipit-source-id: 89a973d067d83439039734712d18727b06d0706d --- sam3/model/io_utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sam3/model/io_utils.py b/sam3/model/io_utils.py index 067f125..9d64c9c 100644 --- a/sam3/model/io_utils.py +++ b/sam3/model/io_utils.py @@ -300,6 +300,12 @@ def load_video_frames_from_video_file_using_cv2( cap.release() pbar.close() + if len(frames) == 0: + raise RuntimeError( + f"No frames could be decoded from video: {video_path}. " + f"The file may be corrupted, empty, or encoded with an unsupported codec." + ) + # Convert to tensor frames_np = np.stack(frames, axis=0).astype(np.float32) # (T, H, W, C) video_tensor = torch.from_numpy(frames_np).permute(0, 3, 1, 2) # (T, C, H, W) From 86ed77094094e5cabb16b0414ec60c5ba9ce0a0f Mon Sep 17 00:00:00 2001 From: Yunseok Jang Date: Mon, 16 Mar 2026 15:38:39 -0700 Subject: [PATCH 16/31] Make decord import lazy and add image-only inference build target Summary: Move the top-level `from decord import cpu, VideoReader` in sam3_image_dataset.py to a lazy import inside the .mp4 video loading block. This avoids import errors for consumers that only need image inference and don't have decord installed. Also add a new `sam3_inference_image_only` BUCK target that excludes decord from pypi_deps for downstream projects that only need image inference. Reviewed By: haithamkhedr Differential Revision: D96809398 fbshipit-source-id: 67c5f0be965d8420b244ff4c45d849314352d3cc --- sam3/train/data/sam3_image_dataset.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sam3/train/data/sam3_image_dataset.py b/sam3/train/data/sam3_image_dataset.py index c956d7b..28e7425 100644 --- a/sam3/train/data/sam3_image_dataset.py +++ b/sam3/train/data/sam3_image_dataset.py @@ -17,7 +17,6 @@ import torch import torch.utils.data import torchvision -from decord import cpu, VideoReader from iopath.common.file_io import g_pathmgr from PIL import Image as PILImage from PIL.Image import DecompressionBombError @@ -202,6 +201,8 @@ def _load_images( try: if ".mp4" in path and path[-4:] == ".mp4": # Going to load a video frame + from decord import cpu, VideoReader + video_path, frame = path.split("@") video = VideoReader(video_path, ctx=cpu(0)) # Convert to PIL image From 9f22cb976fb6e38dad5bb34940fad852dd897d0e Mon Sep 17 00:00:00 2001 From: Arpit Kalla Date: Fri, 27 Mar 2026 10:18:58 -0700 Subject: [PATCH 17/31] SAM 3.1 Release (#503) Summary: SAM 3.1 multiplex video tracking for the OSS sam3 repository. Features: - Multiplex architecture: batch N objects into fixed-capacity buckets for efficient multi-object tracking (16 objects per bucket) - Unified API: build_sam3_predictor(version="sam3.1") with auto-download from HuggingFace (facebook/sam3.1) - Flash Attention 3, torch.compile support (~31 FPS compiled on H100) - Text prompts, point prompts, box prompts, object removal, refinement - Clean inheritance: Sam3MultiplexBase extends Sam3VideoBase - Shared base predictor (Sam3BasePredictor) for both SAM3 and SAM3.1 Performance (H100, 5 objects, compiled): - SAM 3.1: ~31 FPS - Memory: ~17.7 GB Usage: from sam3 import build_sam3_predictor predictor = build_sam3_predictor() # auto-downloads SAM 3.1 from HF Pull Request resolved: https://github.com/facebookresearch/sam3/pull/503 Reviewed By: josephgreer Differential Revision: D98500763 Pulled By: arpitkalla fbshipit-source-id: e2b74568f24bb4ecfb3144a4e8ec559aae6db5e8 --- README.md | 15 +- RELEASE_SAM3p1.md | 150 + assets/sam3.1_diagram.png | Bin 0 -> 102223 bytes assets/sam3.1_efficiency.png | Bin 0 -> 384106 bytes examples/sam3.1_video_predictor_example.ipynb | 643 +++ sam3/__init__.py | 4 +- sam3/agent/agent_core.py | 6 +- sam3/agent/helpers/boxes.py | 20 +- sam3/agent/helpers/masks.py | 8 +- sam3/agent/helpers/rotated_boxes.py | 6 +- sam3/agent/helpers/visualizer.py | 6 +- sam3/eval/cgf1_eval.py | 36 +- sam3/eval/coco_eval.py | 6 +- sam3/eval/coco_writer.py | 6 +- sam3/eval/demo_eval.py | 24 +- sam3/eval/postprocessors.py | 16 +- sam3/eval/ytvis_coco_wrapper.py | 6 +- sam3/eval/ytvis_eval.py | 34 +- sam3/logger.py | 6 +- sam3/model/data_misc.py | 64 +- sam3/model/decoder.py | 447 +- sam3/model/encoder.py | 18 +- sam3/model/geometry_encoders.py | 211 +- sam3/model/io_utils.py | 14 +- sam3/model/maskformer_segmentation.py | 20 +- sam3/model/memory.py | 16 +- sam3/model/model_misc.py | 699 +++- sam3/model/multiplex_mask_decoder.py | 470 +++ sam3/model/multiplex_utils.py | 538 +++ sam3/model/necks.py | 143 + sam3/model/position_encoding.py | 8 +- sam3/model/sam1_task_predictor.py | 30 +- sam3/model/sam3_base_predictor.py | 322 ++ sam3/model/sam3_image.py | 36 +- sam3/model/sam3_image_processor.py | 6 +- sam3/model/sam3_multiplex_base.py | 2856 +++++++++++++ sam3/model/sam3_multiplex_detector.py | 943 +++++ sam3/model/sam3_multiplex_detector_utils.py | 369 ++ sam3/model/sam3_multiplex_tracking.py | 3431 ++++++++++++++++ sam3/model/sam3_multiplex_video_predictor.py | 63 + sam3/model/sam3_tracker_utils.py | 12 +- sam3/model/sam3_video_base.py | 407 +- sam3/model/sam3_video_inference.py | 46 +- sam3/model/sam3_video_predictor.py | 235 +- sam3/model/text_encoder_ve.py | 6 +- sam3/model/video_tracking_multiplex.py | 3654 +++++++++++++++++ sam3/model/video_tracking_multiplex_demo.py | 3476 ++++++++++++++++ sam3/model/vitdet.py | 191 +- sam3/model/vl_combiner.py | 255 +- sam3/model_builder.py | 555 ++- sam3/perflib/compile.py | 54 +- sam3/perflib/connected_components.py | 12 +- sam3/perflib/fused.py | 25 + sam3/perflib/iou.py | 38 + sam3/perflib/masks_ops.py | 21 +- sam3/perflib/triton/connected_components.py | 12 +- sam3/sam/mask_decoder.py | 6 +- sam3/sam/transformer.py | 6 +- sam3/train/data/coco_json_loaders.py | 6 +- sam3/train/data/collator.py | 6 +- sam3/train/data/sam3_image_dataset.py | 12 +- sam3/train/data/sam3_video_dataset.py | 7 +- sam3/train/loss/loss_fns.py | 18 +- sam3/train/masks_ops.py | 26 +- sam3/train/matcher.py | 24 +- sam3/train/optim/optimizer.py | 12 +- sam3/train/train.py | 6 +- sam3/train/trainer.py | 42 +- sam3/train/transforms/basic.py | 6 +- .../transforms/filter_query_transforms.py | 6 +- sam3/train/transforms/point_sampling.py | 6 +- sam3/train/utils/checkpoint_utils.py | 6 +- sam3/train/utils/train_utils.py | 6 +- sam3/visualization_utils.py | 30 +- scripts/eval/silver/download_videos.py | 6 +- scripts/eval/silver/extract_frames.py | 6 +- .../eval/veval/saco_yt1b_frame_prep_util.py | 12 +- scripts/measure_speed.py | 304 ++ scripts/qualitative_test.py | 312 ++ 79 files changed, 20773 insertions(+), 793 deletions(-) create mode 100644 RELEASE_SAM3p1.md create mode 100644 assets/sam3.1_diagram.png create mode 100644 assets/sam3.1_efficiency.png create mode 100644 examples/sam3.1_video_predictor_example.ipynb create mode 100644 sam3/model/multiplex_mask_decoder.py create mode 100644 sam3/model/multiplex_utils.py create mode 100644 sam3/model/sam3_base_predictor.py create mode 100644 sam3/model/sam3_multiplex_base.py create mode 100644 sam3/model/sam3_multiplex_detector.py create mode 100644 sam3/model/sam3_multiplex_detector_utils.py create mode 100644 sam3/model/sam3_multiplex_tracking.py create mode 100644 sam3/model/sam3_multiplex_video_predictor.py create mode 100644 sam3/model/video_tracking_multiplex.py create mode 100644 sam3/model/video_tracking_multiplex_demo.py create mode 100644 sam3/perflib/fused.py create mode 100644 sam3/perflib/iou.py create mode 100644 scripts/measure_speed.py create mode 100644 scripts/qualitative_test.py diff --git a/README.md b/README.md index 669242d..9aad534 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,13 @@ This breakthrough is driven by an innovative data engine that has automatically

+## Latest updates + +**03/27/2026 -- SAM 3.1 Object Multiplex is released. It introduces a shared-memory approach for joint multi-object tracking that is significantly faster without sacrificing accuracy.** + +- A new suite of improved model checkpoints (denoted as **SAM 3.1**) are released on [Hugging Face](https://huggingface.co/facebook/sam3.1). See [`RELEASE_SAM3p1.md`](RELEASE_SAM3p1.md) for full details. + * To use the new SAM 3.1 checkpoints, you need the latest model code from this repo. If you have installed an earlier version of this repo, pull the latest code from this repo (with `git pull`), and then reinstall the repo following [Installation](#installation) below. + ## Installation ### Prerequisites @@ -74,7 +81,7 @@ conda activate sam3 2. **Install PyTorch with CUDA support:** ```bash -pip install torch==2.7.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126 +pip install torch==2.10.0 torchvision --index-url https://download.pytorch.org/whl/cu128 ``` 3. **Clone the repository and install the package:** @@ -95,6 +102,12 @@ pip install -e ".[notebooks]" pip install -e ".[train,dev]" ``` +5. **Optional dependencies for faster inference** +```bash +pip install einops ninja && pip install flash-attn-3 --no-deps --index-url https://download.pytorch.org/whl/cu128 +pip install git+https://github.com/ronghanghu/cc_torch.git +``` + ## Getting Started ⚠️ Before using SAM 3, please request access to the checkpoints on the SAM 3 diff --git a/RELEASE_SAM3p1.md b/RELEASE_SAM3p1.md new file mode 100644 index 0000000..cf4f66f --- /dev/null +++ b/RELEASE_SAM3p1.md @@ -0,0 +1,150 @@ +# Release Notes + +## SAM 3.1 — March 27, 2026 + +SAM 3.1 introduces **Object Multiplex**, a shared-memory approach for joint multi-object tracking that is significantly faster without sacrificing accuracy. This release also includes new model checkpoints and optimized inference. + +### Object Multiplex + +SAM 3's video pipeline processes each tracked object independently, which scales linearly with the number of objects. Object Multiplex groups objects into fixed-capacity buckets and processes them jointly, drastically reducing redundant computation. For technical details, see Appendix H (Object Multiplex) in the [SAM 3 paper](https://arxiv.org/abs/2511.16719). + +

+ +

+ +#### Key Improvements +- **~7x speedup** at 128 objects on a single H100 GPU compared to the SAM 3 November 2025 release +- Inference optimizations that significantly improve multi-object tracking efficiency: + - Reduced CPU-GPU synchronization in detection-tracker association and other heuristics + - Enhanced `torch.compile` support with improved operation fusion + - Batched postprocessing and vision encoder to increase GPU utilization +- Mixed results on SA-Co/VEval video benchmarks, with notable improvement on YT-Temporal-1B (+2.1 cgF1) +- Improved VOS performance on 6 out of 7 benchmarks, including +2.0 on the challenging MOSEv2 + +#### Inference Efficiency + +

+ +

+ +#### Video PCS with Text Prompt + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ModelSA-Co/VEval benchmark test splitPublic benchmarks
SA-VYT-Temporal-1BSmartGlassesLVVISBURSTYTVIS21OVIS
cgF1pHOTAcgF1pHOTAcgF1pHOTAtest mAPtest HOTAval mAPval mAP
SAM 330.358.050.869.936.463.636.344.557.460.5
SAM 3.130.558.752.970.736.364.434.343.356.661.5
+ +
+ +#### Video Object Segmentation (VOS) + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ModelJ&FGJ&Ḟ
MOSEv1 valDAVIS17 valLVOSv2 valSA-V valSA-V testYTVOS19 valMOSEv2 val
SAM 378.492.288.583.584.489.760.3
SAM 3.179.692.789.283.885.189.362.3
+
+ +### New Checkpoints + +The SAM 3.1 checkpoints are available on the [Hugging Face repo](https://huggingface.co/facebook/sam3.1). See [Getting Started](README.md#getting-started) for download and authentication instructions. + +### Notebooks + +- [`sam3.1_video_predictor_example.ipynb`](examples/sam3.1_video_predictor_example.ipynb): Demonstrates how to use SAM 3.1 with Object Multiplex for video segmentation and dense tracking with text and point prompts. + +### Contributors + +[Arpit Kalla](https://github.com/arpitkalla), [Chaitanya Ryali](https://scholar.google.com/citations?user=4LWx24UAAAAJ&hl=en), [Christian Puhrsch](https://github.com/cpuhrsch), [Ho Kei Cheng](https://hkchengrex.com/), [Joseph Greer](https://scholar.google.com/citations?user=guL96CkAAAAJ&hl=en), [Meng Wang](https://github.com/mengwa41), [Miran Heo](https://sites.google.com/view/miranheo), [Pengchuan Zhang](https://pzzhang.github.io/pzzhang/), [Roman Rädle](https://scholar.google.com/citations?user=Tpt57v0AAAAJ&hl=en), [Yuan-Ting Hu](https://scholar.google.com/citations?user=E8DVVYQAAAAJ&hl=en) diff --git a/assets/sam3.1_diagram.png b/assets/sam3.1_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..c08585d3846a2db6239fb529a9ba944df927f82e GIT binary patch literal 102223 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

44g2nvtY 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 z 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 U8N=zl8+>}Zr>mdKI;Vst0ChlVkpKVy literal 0 HcmV?d00001 diff --git a/examples/sam3.1_video_predictor_example.ipynb b/examples/sam3.1_video_predictor_example.ipynb new file mode 100644 index 0000000..3867357 --- /dev/null +++ b/examples/sam3.1_video_predictor_example.ipynb @@ -0,0 +1,643 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright (c) Meta Platforms, Inc. and affiliates." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Video segmentation and tracking with SAM 3.1 (Object Multiplex)\n", + "\n", + "This notebook demonstrates how to use SAM 3.1 with Object Multiplex for interactive video segmentation and dense tracking. Object Multiplex groups objects into fixed-capacity buckets and processes them jointly, drastically reducing redundant computation compared to SAM 3's per-object inference. It covers the following capabilities:\n", + "\n", + "- **Text prompts**: Using natural language descriptions to segment objects (e.g., \"person\", \"shoe\")\n", + "- **Point prompts**: Adding positive/negative clicks to segment and refine objects\n", + "\n", + "We use the terms _segment_ or _mask_ to refer to the model prediction for an object on a single frame, and _masklet_ to refer to the spatio-temporal masks across the entire video. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + " \"Open\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "using_colab = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if using_colab:\n", + " import torch\n", + " import torchvision\n", + " print(\"PyTorch version:\", torch.__version__)\n", + " print(\"Torchvision version:\", torchvision.__version__)\n", + " print(\"CUDA is available:\", torch.cuda.is_available())\n", + " import sys\n", + " !{sys.executable} -m pip install opencv-python matplotlib scikit-learn\n", + " !{sys.executable} -m pip install 'git+https://github.com/facebookresearch/sam3.git'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set-up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sam3\n", + "import torch\n", + "\n", + "sam3_root = os.path.join(os.path.dirname(sam3.__file__), \"..\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Inference and visualization utils" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from sam3.model_builder import build_sam3_multiplex_video_predictor\n", + "\n", + "predictor = build_sam3_multiplex_video_predictor()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [], + "source": [ + "import glob\n", + "import os\n", + "\n", + "import cv2\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from PIL import Image\n", + "from sam3.visualization_utils import (\n", + " load_frame,\n", + " prepare_masks_for_visualization,\n", + " visualize_formatted_frame_output,\n", + ")\n", + "\n", + "plt.rcParams[\"axes.titlesize\"] = 12\n", + "plt.rcParams[\"figure.titlesize\"] = 12\n", + "\n", + "\n", + "def propagate_in_video(predictor, session_id):\n", + " outputs_per_frame = {}\n", + " for response in predictor.handle_stream_request(\n", + " request=dict(\n", + " type=\"propagate_in_video\",\n", + " session_id=session_id,\n", + " )\n", + " ):\n", + " outputs_per_frame[response[\"frame_index\"]] = response[\"outputs\"]\n", + "\n", + " return outputs_per_frame\n", + "\n", + "\n", + "def abs_to_rel_coords(coords, IMG_WIDTH, IMG_HEIGHT, coord_type=\"point\"):\n", + " \"\"\"Convert absolute coordinates to relative coordinates (0-1 range)\n", + "\n", + " Args:\n", + " coords: List of coordinates\n", + " coord_type: 'point' for [x, y] or 'box' for [x, y, w, h]\n", + " \"\"\"\n", + " if coord_type == \"point\":\n", + " return [[x / IMG_WIDTH, y / IMG_HEIGHT] for x, y in coords]\n", + " elif coord_type == \"box\":\n", + " return [\n", + " [x / IMG_WIDTH, y / IMG_HEIGHT, w / IMG_WIDTH, h / IMG_HEIGHT]\n", + " for x, y, w, h in coords\n", + " ]\n", + " else:\n", + " raise ValueError(f\"Unknown coord_type: {coord_type}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Loading an example video\n", + "\n", + "We assume that the video is stored as either **a list of JPEG frames with filenames like `.jpg`** or **an MP4 video**.\n", + "\n", + "Note that you can extract their JPEG frames using ffmpeg (https://ffmpeg.org/) as follows:\n", + "```\n", + "ffmpeg -i .mp4 -q:v 2 -start_number 0 /'%05d.jpg'\n", + "```\n", + "where `-q:v` generates high-quality JPEG frames and `-start_number 0` asks ffmpeg to start the JPEG file from `00000.jpg`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# \"video_path\" needs to be either a JPEG folder or a MP4 video file\n", + "video_path = f\"{sam3_root}/assets/videos/0001\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# load \"video_frames_for_vis\" for visualization purposes (they are not used by the model)\n", + "if isinstance(video_path, str) and video_path.endswith(\".mp4\"):\n", + " cap = cv2.VideoCapture(video_path)\n", + " video_frames_for_vis = []\n", + " while True:\n", + " ret, frame = cap.read()\n", + " if not ret:\n", + " break\n", + " video_frames_for_vis.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))\n", + " cap.release()\n", + "else:\n", + " video_frames_for_vis = glob.glob(os.path.join(video_path, \"*.jpg\"))\n", + " try:\n", + " video_frames_for_vis.sort(\n", + " key=lambda p: int(os.path.splitext(os.path.basename(p))[0])\n", + " )\n", + " except ValueError:\n", + " print(\n", + " f'frame names are not in \".jpg\" format: {video_frames_for_vis[:5]=}, '\n", + " f\"falling back to lexicographic sort.\"\n", + " )\n", + " video_frames_for_vis.sort()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Opening an inference session on this video\n", + "\n", + "SAM 3.1 requires stateful inference for interactive video segmentation, so we need to initialize an **inference session** on this video.\n", + "\n", + "During initialization, it loads all the video frames and stores their pixels in the session state." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "response = predictor.handle_request(\n", + " request=dict(\n", + " type=\"start_session\",\n", + " resource_path=video_path,\n", + " )\n", + ")\n", + "session_id = response[\"session_id\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Video promptable concept segmentation with text\n", + "\n", + "Using SAM 3.1 you can describe objects using natural language, and the model will automatically detect and track all instances of that object throughout the video.\n", + "\n", + "In the example below, we add a text prompt on frame 0 and propagation throughout the video. Here we use the text prompt \"person\" to detect all people in the video. SAM 3.1 will automatically identify multiple person instances and assign each a unique object ID.\n", + "\n", + "Note that the first call might be slower due to setting up buffers. **You can rerun all the cells below when measuring speed.**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# note: in case you already ran one text prompt and now want to switch to another text prompt\n", + "# it's required to reset the session first (otherwise the results would be wrong)\n", + "_ = predictor.handle_request(\n", + " request=dict(\n", + " type=\"reset_session\",\n", + " session_id=session_id,\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "prompt_text_str = \"person\"\n", + "frame_idx = 0 # add a text prompt on frame 0\n", + "response = predictor.handle_request(\n", + " request=dict(\n", + " type=\"add_prompt\",\n", + " session_id=session_id,\n", + " frame_index=frame_idx,\n", + " text=prompt_text_str,\n", + " )\n", + ")\n", + "out = response[\"outputs\"]\n", + "\n", + "plt.close(\"all\")\n", + "visualize_formatted_frame_output(\n", + " frame_idx,\n", + " video_frames_for_vis,\n", + " outputs_list=[prepare_masks_for_visualization({frame_idx: out})],\n", + " titles=[\"SAM 3.1 Dense Tracking outputs\"],\n", + " figsize=(6, 4),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# now we propagate the outputs from frame 0 to the end of the video and collect all outputs\n", + "outputs_per_frame = propagate_in_video(predictor, session_id)\n", + "\n", + "# finally, we reformat the outputs for visualization and plot the outputs every 60 frames\n", + "outputs_per_frame = prepare_masks_for_visualization(outputs_per_frame)\n", + "\n", + "vis_frame_stride = 60\n", + "plt.close(\"all\")\n", + "for frame_idx in range(0, len(outputs_per_frame), vis_frame_stride):\n", + " visualize_formatted_frame_output(\n", + " frame_idx,\n", + " video_frames_for_vis,\n", + " outputs_list=[outputs_per_frame],\n", + " titles=[\"SAM 3.1 Dense Tracking outputs\"],\n", + " figsize=(6, 4),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Removing objects\n", + "\n", + "We can remove individual objects using their id.\n", + "\n", + "As an example, let's remove object 2 (which is the dancer in the front)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# we pick id 2, which is the dancer in the front\n", + "obj_id = 2\n", + "response = predictor.handle_request(\n", + " request=dict(\n", + " type=\"remove_object\",\n", + " session_id=session_id,\n", + " obj_id=obj_id,\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# now we propagate the outputs from frame 0 to the end of the video and collect all outputs\n", + "outputs_per_frame = propagate_in_video(predictor, session_id)\n", + "\n", + "# finally, we reformat the outputs for visualization and plot the outputs every 60 frames\n", + "outputs_per_frame = prepare_masks_for_visualization(outputs_per_frame)\n", + "\n", + "vis_frame_stride = 60\n", + "plt.close(\"all\")\n", + "for frame_idx in range(0, len(outputs_per_frame), vis_frame_stride):\n", + " visualize_formatted_frame_output(\n", + " frame_idx,\n", + " video_frames_for_vis,\n", + " outputs_list=[outputs_per_frame],\n", + " titles=[\"SAM 3.1 Dense Tracking outputs\"],\n", + " figsize=(6, 4),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Adding new objects with point prompts\n", + "\n", + "We can add new objects through point prompts.\n", + "\n", + "Assuming that we've changed our mind, and now that we want to add back the dancer in the front (whom we just removed in the step above). We can use interactive clicks to add her back." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sample_img = Image.fromarray(load_frame(video_frames_for_vis[0]))\n", + "\n", + "IMG_WIDTH, IMG_HEIGHT = sample_img.size" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# let's add back the dancer via point prompts.\n", + "# we will use a single positive click to add the dancer back.\n", + "\n", + "frame_idx = 0\n", + "obj_id = 2\n", + "points_abs = np.array(\n", + " [\n", + " [760, 550], # positive click\n", + " ]\n", + ")\n", + "# positive clicks have label 1, while negative clicks have label 0\n", + "labels = np.array([1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# convert points and labels to tensors; also convert to relative coordinates\n", + "points_tensor = torch.tensor(\n", + " abs_to_rel_coords(points_abs, IMG_WIDTH, IMG_HEIGHT, coord_type=\"point\"),\n", + " dtype=torch.float32,\n", + ")\n", + "points_labels_tensor = torch.tensor(labels, dtype=torch.int32)\n", + "\n", + "response = predictor.handle_request(\n", + " request=dict(\n", + " type=\"add_prompt\",\n", + " session_id=session_id,\n", + " frame_index=frame_idx,\n", + " points=points_tensor,\n", + " point_labels=points_labels_tensor,\n", + " obj_id=obj_id,\n", + " )\n", + ")\n", + "out = response[\"outputs\"]\n", + "\n", + "plt.close(\"all\")\n", + "visualize_formatted_frame_output(\n", + " frame_idx,\n", + " video_frames_for_vis,\n", + " outputs_list=[prepare_masks_for_visualization({frame_idx: out})],\n", + " titles=[\"SAM 3.1 Dense Tracking outputs\"],\n", + " figsize=(6, 4),\n", + " points_list=[points_abs],\n", + " points_labels_list=[labels],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# now we propagate the outputs from frame 0 to the end of the video and collect all outputs\n", + "outputs_per_frame = propagate_in_video(predictor, session_id)\n", + "\n", + "# finally, we reformat the outputs for visualization and plot the outputs every 60 frames\n", + "outputs_per_frame = prepare_masks_for_visualization(outputs_per_frame)\n", + "\n", + "vis_frame_stride = 60\n", + "plt.close(\"all\")\n", + "for frame_idx in range(0, len(outputs_per_frame), vis_frame_stride):\n", + " visualize_formatted_frame_output(\n", + " frame_idx,\n", + " video_frames_for_vis,\n", + " outputs_list=[outputs_per_frame],\n", + " titles=[\"SAM 3.1 Dense Tracking outputs\"],\n", + " figsize=(6, 4),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Refining an existing object with point prompts\n", + "\n", + "We can also refine the segmentation mask of an existing object through point prompts.\n", + "\n", + "Assuming that we've changed our mind (again) -- for Object ID 2 (the dancer in the front whom we just added back in the step above), now we only want to segment her T-shirt instead of her whole body. We can adjust the segmentation mask with a few more positive and negative clicks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# For the dancer in the front, suppose now we only want to segment her T-shirt instead of her whole body\n", + "# we will use 2 positive clicks and 2 negative clicks to select her shirt.\n", + "\n", + "refine_object_3 = True\n", + "\n", + "if refine_object_3:\n", + " frame_idx = 0\n", + " obj_id = 3\n", + " points_abs = np.array(\n", + " [\n", + " [800, 135], # positive click\n", + " [800, 180], # negative click\n", + " ]\n", + " )\n", + " # positive clicks have label 1, while negative clicks have label 0\n", + " labels = np.array([1, 0])\n", + " \n", + "else:\n", + " frame_idx = 0\n", + " obj_id = 2\n", + " points_abs = np.array(\n", + " [\n", + " [740, 450], # positive click\n", + " [760, 630], # negative click\n", + " [840, 640], # negative click\n", + " [760, 550], # positive click\n", + " ]\n", + " )\n", + " # positive clicks have label 1, while negative clicks have label 0\n", + " labels = np.array([1, 0, 0, 1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# convert points and labels to tensors; also convert to relative coordinates\n", + "points_tensor = torch.tensor(\n", + " abs_to_rel_coords(points_abs, IMG_WIDTH, IMG_HEIGHT, coord_type=\"point\"),\n", + " dtype=torch.float32,\n", + ")\n", + "points_labels_tensor = torch.tensor(labels, dtype=torch.int32)\n", + "\n", + "response = predictor.handle_request(\n", + " request=dict(\n", + " type=\"add_prompt\",\n", + " session_id=session_id,\n", + " frame_index=frame_idx,\n", + " points=points_tensor,\n", + " point_labels=points_labels_tensor,\n", + " obj_id=obj_id,\n", + " )\n", + ")\n", + "out = response[\"outputs\"]\n", + "\n", + "plt.close(\"all\")\n", + "visualize_formatted_frame_output(\n", + " frame_idx,\n", + " video_frames_for_vis,\n", + " outputs_list=[prepare_masks_for_visualization({frame_idx: out})],\n", + " titles=[\"SAM 3.1 Dense Tracking outputs\"],\n", + " figsize=(6, 4),\n", + " points_list=[points_abs],\n", + " points_labels_list=[labels],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# now we propagate the outputs from frame 0 to the end of the video and collect all outputs\n", + "outputs_per_frame = propagate_in_video(predictor, session_id)\n", + "\n", + "# finally, we reformat the outputs for visualization and plot the outputs every 60 frames\n", + "outputs_per_frame = prepare_masks_for_visualization(outputs_per_frame)\n", + "\n", + "vis_frame_stride = 60\n", + "plt.close(\"all\")\n", + "for frame_idx in range(0, len(outputs_per_frame), vis_frame_stride):\n", + " visualize_formatted_frame_output(\n", + " frame_idx,\n", + " video_frames_for_vis,\n", + " outputs_list=[outputs_per_frame],\n", + " titles=[\"SAM 3.1 Dense Tracking outputs\"],\n", + " figsize=(6, 4),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Close session\n", + "\n", + "Each session is tied to a single video. We can close the session after inference to free up its resources.\n", + "\n", + "(Then, you may start a new session on another video.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "_ = predictor.handle_request(\n", + " request=dict(\n", + " type=\"close_session\",\n", + " session_id=session_id,\n", + " )\n", + ")" + ] + } + ], + "metadata": { + "fileHeader": "", + "fileUid": "0c5b0843-1bcb-4dac-8c85-3f149debb325", + "isAdHoc": false, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/sam3/__init__.py b/sam3/__init__.py index 1e75971..3600339 100644 --- a/sam3/__init__.py +++ b/sam3/__init__.py @@ -2,8 +2,8 @@ # pyre-unsafe -from .model_builder import build_sam3_image_model +from .model_builder import build_sam3_image_model, build_sam3_predictor __version__ = "0.1.0" -__all__ = ["build_sam3_image_model"] +__all__ = ["build_sam3_image_model", "build_sam3_predictor"] diff --git a/sam3/agent/agent_core.py b/sam3/agent/agent_core.py index 8b4fc8a..27e5120 100644 --- a/sam3/agent/agent_core.py +++ b/sam3/agent/agent_core.py @@ -296,9 +296,9 @@ def agent_inference( assert LATEST_SAM3_TEXT_PROMPT != "" # Make sure that the last message is a image - assert messages[-1]["content"][1]["type"] == "image", ( - "Second content element should be an image" - ) + assert ( + messages[-1]["content"][1]["type"] == "image" + ), "Second content element should be an image" messages.pop() # Remove the last user message # Add simplified replacement message simplified_message = { diff --git a/sam3/agent/helpers/boxes.py b/sam3/agent/helpers/boxes.py index df44769..a32e520 100755 --- a/sam3/agent/helpers/boxes.py +++ b/sam3/agent/helpers/boxes.py @@ -84,9 +84,9 @@ def convert( ], "Relative mode not yet supported!" if from_mode == BoxMode.XYWHA_ABS and to_mode == BoxMode.XYXY_ABS: - assert arr.shape[-1] == 5, ( - "The last dimension of input shape must be 5 for XYWHA format" - ) + assert ( + arr.shape[-1] == 5 + ), "The last dimension of input shape must be 5 for XYWHA format" original_dtype = arr.dtype arr = arr.double() @@ -244,9 +244,9 @@ def __getitem__(self, item) -> "Boxes": if isinstance(item, int): return Boxes(self.tensor[item].view(1, -1)) b = self.tensor[item] - assert b.dim() == 2, ( - "Indexing on Boxes with {} failed to return a matrix!".format(item) - ) + assert ( + b.dim() == 2 + ), "Indexing on Boxes with {} failed to return a matrix!".format(item) return Boxes(b) def __len__(self) -> int: @@ -424,10 +424,10 @@ def matched_pairwise_iou(boxes1: Boxes, boxes2: Boxes) -> torch.Tensor: Returns: Tensor: iou, sized [N]. """ - assert len(boxes1) == len(boxes2), ( - "boxlists should have the samenumber of entries, got {}, {}".format( - len(boxes1), len(boxes2) - ) + assert len(boxes1) == len( + boxes2 + ), "boxlists should have the samenumber of entries, got {}, {}".format( + len(boxes1), len(boxes2) ) area1 = boxes1.area() # [N] area2 = boxes2.area() # [N] diff --git a/sam3/agent/helpers/masks.py b/sam3/agent/helpers/masks.py index a303f6e..ba10fc8 100755 --- a/sam3/agent/helpers/masks.py +++ b/sam3/agent/helpers/masks.py @@ -141,10 +141,10 @@ def __getitem__(self, item: Union[int, slice, torch.BoolTensor]) -> "BitMasks": if isinstance(item, int): return BitMasks(self.tensor[item].unsqueeze(0)) m = self.tensor[item] - assert m.dim() == 3, ( - "Indexing on BitMasks with {} returns a tensor with shape {}!".format( - item, m.shape - ) + assert ( + m.dim() == 3 + ), "Indexing on BitMasks with {} returns a tensor with shape {}!".format( + item, m.shape ) return BitMasks(m) diff --git a/sam3/agent/helpers/rotated_boxes.py b/sam3/agent/helpers/rotated_boxes.py index 0017335..cd39af8 100755 --- a/sam3/agent/helpers/rotated_boxes.py +++ b/sam3/agent/helpers/rotated_boxes.py @@ -363,9 +363,9 @@ def __getitem__(self, item) -> "RotatedBoxes": if isinstance(item, int): return RotatedBoxes(self.tensor[item].view(1, -1)) b = self.tensor[item] - assert b.dim() == 2, ( - "Indexing on RotatedBoxes with {} failed to return a matrix!".format(item) - ) + assert ( + b.dim() == 2 + ), "Indexing on RotatedBoxes with {} failed to return a matrix!".format(item) return RotatedBoxes(b) def __len__(self) -> int: diff --git a/sam3/agent/helpers/visualizer.py b/sam3/agent/helpers/visualizer.py index 2050652..bab3dff 100755 --- a/sam3/agent/helpers/visualizer.py +++ b/sam3/agent/helpers/visualizer.py @@ -221,9 +221,9 @@ def non_empty_mask(self): empty_ids.append(id) if len(empty_ids) == 0: return np.zeros(self._seg.shape, dtype=np.uint8) - assert len(empty_ids) == 1, ( - ">1 ids corresponds to no labels. This is currently not supported" - ) + assert ( + len(empty_ids) == 1 + ), ">1 ids corresponds to no labels. This is currently not supported" return (self._seg != empty_ids[0]).numpy().astype(np.bool) def semantic_masks(self): diff --git a/sam3/eval/cgf1_eval.py b/sam3/eval/cgf1_eval.py index cbc91c1..71fe2ea 100644 --- a/sam3/eval/cgf1_eval.py +++ b/sam3/eval/cgf1_eval.py @@ -126,9 +126,9 @@ def loadRes(self, resFile): # MODIFICATION: faster and cached subset check if not hasattr(self, "img_id_set"): self.img_id_set = set(self.getImgIds()) - assert set(annsImgIds).issubset(self.img_id_set), ( - "Results do not correspond to current coco set" - ) + assert set(annsImgIds).issubset( + self.img_id_set + ), "Results do not correspond to current coco set" # END MODIFICATION if "caption" in anns[0]: imgIds = set([img["id"] for img in res.dataset["images"]]) & set( @@ -301,9 +301,9 @@ def evaluateImg(self, imgId, catId, aRng, maxDet): TP = (match_scores >= thresh).sum() FP = len(dt) - TP FN = len(gt) - TP - assert FP >= 0 and FN >= 0, ( - f"FP: {FP}, FN: {FN}, TP: {TP}, match_scores: {match_scores}, len(dt): {len(dt)}, len(gt): {len(gt)}, ious: {ious}" - ) + assert ( + FP >= 0 and FN >= 0 + ), f"FP: {FP}, FN: {FN}, TP: {TP}, match_scores: {match_scores}, len(dt): {len(dt)}, len(gt): {len(gt)}, ious: {ious}" TPs.append(TP) FPs.append(FP) FNs.append(FN) @@ -599,9 +599,9 @@ def evaluate(self, pred_file: str): """ assert len(self.coco_gts) > 0, "No ground truth provided for evaluation." - assert len(self.coco_gts) == len(self.coco_evals), ( - "Mismatch in number of ground truths and evaluators." - ) + assert len(self.coco_gts) == len( + self.coco_evals + ), "Mismatch in number of ground truths and evaluators." if self.verbose: print(f"Loading predictions from {pred_file}") @@ -668,17 +668,17 @@ def _select_best_scoring(scorings): if len(scorings) == 1: return scorings[0] - assert scorings[0].ndim == 3, ( - f"Expecting results in [numCats, numAreas, numImgs] format, got {scorings[0].shape}" - ) - assert scorings[0].shape[0] == 1, ( - f"Expecting a single category, got {scorings[0].shape[0]}" - ) + assert ( + scorings[0].ndim == 3 + ), f"Expecting results in [numCats, numAreas, numImgs] format, got {scorings[0].shape}" + assert ( + scorings[0].shape[0] == 1 + ), f"Expecting a single category, got {scorings[0].shape[0]}" for scoring in scorings: - assert scoring.shape == scorings[0].shape, ( - f"Shape mismatch: {scoring.shape}, {scorings[0].shape}" - ) + assert ( + scoring.shape == scorings[0].shape + ), f"Shape mismatch: {scoring.shape}, {scorings[0].shape}" selected_imgs = [] for img_id in range(scorings[0].shape[-1]): diff --git a/sam3/eval/coco_eval.py b/sam3/eval/coco_eval.py index 7eee615..3716885 100644 --- a/sam3/eval/coco_eval.py +++ b/sam3/eval/coco_eval.py @@ -751,9 +751,9 @@ def loadRes(self, resFile): anns = resFile assert type(anns) == list, "results in not an array of objects" annsImgIds = [ann["image_id"] for ann in anns] - assert set(annsImgIds) == (set(annsImgIds) & set(self.getImgIds())), ( - "Results do not correspond to current coco set" - ) + assert set(annsImgIds) == ( + set(annsImgIds) & set(self.getImgIds()) + ), "Results do not correspond to current coco set" if "caption" in anns[0]: imgIds = set([img["id"] for img in res.dataset["images"]]) & set( [ann["image_id"] for ann in anns] diff --git a/sam3/eval/coco_writer.py b/sam3/eval/coco_writer.py index f49fcf2..54f9307 100644 --- a/sam3/eval/coco_writer.py +++ b/sam3/eval/coco_writer.py @@ -83,9 +83,9 @@ def __init__( self.merge_predictions = merge_predictions self.pred_file_evaluators = pred_file_evaluators if self.pred_file_evaluators is not None: - assert merge_predictions, ( - "merge_predictions must be True if pred_file_evaluators are provided" - ) + assert ( + merge_predictions + ), "merge_predictions must be True if pred_file_evaluators are provided" assert self.dump_dir is not None, "dump_dir must be provided" if is_main_process(): diff --git a/sam3/eval/demo_eval.py b/sam3/eval/demo_eval.py index 6ac7063..353b86a 100644 --- a/sam3/eval/demo_eval.py +++ b/sam3/eval/demo_eval.py @@ -154,9 +154,9 @@ def evaluateImg(self, imgId, catId, aRng, maxDet): TP = (match_scores >= thresh).sum() FP = len(dt) - TP FN = len(gt) - TP - assert FP >= 0 and FN >= 0, ( - f"FP: {FP}, FN: {FN}, TP: {TP}, match_scores: {match_scores}, len(dt): {len(dt)}, len(gt): {len(gt)}, ious: {ious}" - ) + assert ( + FP >= 0 and FN >= 0 + ), f"FP: {FP}, FN: {FN}, TP: {TP}, match_scores: {match_scores}, len(dt): {len(dt)}, len(gt): {len(gt)}, ious: {ious}" TPs.append(TP) FPs.append(FP) FNs.append(FN) @@ -526,17 +526,17 @@ def select_best_scoring(self, scorings): if len(scorings) == 1: return scorings[0] - assert scorings[0].ndim == 3, ( - f"Expecting results in [numCats, numAreas, numImgs] format, got {scorings[0].shape}" - ) - assert scorings[0].shape[0] == 1, ( - f"Expecting a single category, got {scorings[0].shape[0]}" - ) + assert ( + scorings[0].ndim == 3 + ), f"Expecting results in [numCats, numAreas, numImgs] format, got {scorings[0].shape}" + assert ( + scorings[0].shape[0] == 1 + ), f"Expecting a single category, got {scorings[0].shape[0]}" for scoring in scorings: - assert scoring.shape == scorings[0].shape, ( - f"Shape mismatch: {scoring.shape}, {scorings[0].shape}" - ) + assert ( + scoring.shape == scorings[0].shape + ), f"Shape mismatch: {scoring.shape}, {scorings[0].shape}" selected_imgs = [] for img_id in range(scorings[0].shape[-1]): diff --git a/sam3/eval/postprocessors.py b/sam3/eval/postprocessors.py index 44eb103..7bd26a9 100644 --- a/sam3/eval/postprocessors.py +++ b/sam3/eval/postprocessors.py @@ -83,9 +83,9 @@ def forward( ret_tensordict: Experimental argument. If true, return a tensordict.TensorDict instead of a list of dictionaries for easier manipulation. """ if ret_tensordict: - assert consistent is True, ( - "We don't support returning TensorDict if the outputs have different shapes" - ) # NOTE: It's possible but we don't support it. + assert ( + consistent is True + ), "We don't support returning TensorDict if the outputs have different shapes" # NOTE: It's possible but we don't support it. assert self.detection_threshold <= 0.0, "TODO: implement?" try: from tensordict import TensorDict @@ -118,9 +118,7 @@ def forward( if boxes is None: assert out_masks is not None - assert not ret_tensordict, ( - "We don't support returning TensorDict if the output does not contain boxes" - ) + assert not ret_tensordict, "We don't support returning TensorDict if the output does not contain boxes" B = len(out_masks) boxes = [None] * B scores = [None] * B @@ -420,9 +418,9 @@ def process_results( if video_id == -1: video_id = unique_vid_id.item() else: - assert video_id == unique_vid_id.item(), ( - "We can only postprocess one video per datapoint" - ) + assert ( + video_id == unique_vid_id.item() + ), "We can only postprocess one video per datapoint" # keeping track of which objects appear in the current frame obj_ids_per_frame = frame_outs["pred_object_ids"] assert obj_ids_per_frame.size(-1) == frame_outs["pred_logits"].size(-2) diff --git a/sam3/eval/ytvis_coco_wrapper.py b/sam3/eval/ytvis_coco_wrapper.py index 25feda4..5412d69 100644 --- a/sam3/eval/ytvis_coco_wrapper.py +++ b/sam3/eval/ytvis_coco_wrapper.py @@ -95,9 +95,9 @@ def loadRes(self, resFile): anns = resFile assert type(anns) == list, "results is not an array of objects" annsImgIds = [ann["image_id"] for ann in anns] - assert set(annsImgIds) == (set(annsImgIds) & set(self.getImgIds())), ( - "Results do not correspond to current coco set" - ) + assert set(annsImgIds) == ( + set(annsImgIds) & set(self.getImgIds()) + ), "Results do not correspond to current coco set" if "bboxes" in anns[0] and not anns[0]["bboxes"] == []: res.dataset["categories"] = copy.deepcopy(self.dataset["categories"]) for id, ann in enumerate(anns): diff --git a/sam3/eval/ytvis_eval.py b/sam3/eval/ytvis_eval.py index 0754b62..2ff2309 100644 --- a/sam3/eval/ytvis_eval.py +++ b/sam3/eval/ytvis_eval.py @@ -109,7 +109,9 @@ def iou_tracklets(preds, gts): ) # Num preds x Num GTS x Num frames inter = inter.sum(-1) union = union.sum(-1) - assert (union > 0).all(), ( + assert ( + union > 0 + ).all(), ( "There exists a tracklet with zero GTs across time. This is suspicious" ) return inter / union @@ -134,9 +136,9 @@ def iou_masklets(preds, gts): iou = inter / union assert iou >= 0 and iou <= 1, "Encountered an error in IoU computation" else: - assert np.isclose(inter, 0) and np.isclose(union, 0), ( - "Encountered an error in IoU computation" - ) + assert np.isclose(inter, 0) and np.isclose( + union, 0 + ), "Encountered an error in IoU computation" iou = 1 return iou @@ -204,16 +206,16 @@ def prepare(self, predictions): if len(prediction) == 0: continue for k in ["boxes", "scores", "labels"]: - assert k in prediction, ( - f"Expected predictions to have `{k}` key, available keys are {prediction.keys()}" - ) + assert ( + k in prediction + ), f"Expected predictions to have `{k}` key, available keys are {prediction.keys()}" if self.save_per_frame_scores: - assert "per_frame_scores" in prediction, ( - f"Expected predictions to have `per_frame_scores` key, available keys are {prediction.keys()}" - ) - assert xor("masks" in prediction, "masks_rle" in prediction), ( - f"Expected predictions to have either `masks` key or `masks_rle` key, available keys are {prediction.keys()}" - ) + assert ( + "per_frame_scores" in prediction + ), f"Expected predictions to have `per_frame_scores` key, available keys are {prediction.keys()}" + assert xor( + "masks" in prediction, "masks_rle" in prediction + ), f"Expected predictions to have either `masks` key or `masks_rle` key, available keys are {prediction.keys()}" boxes = prediction["boxes"] boxes = convert_to_xywh(boxes).tolist() @@ -221,9 +223,9 @@ def prepare(self, predictions): labels = prediction["labels"].tolist() if "masks" in prediction: masks = prediction["masks"].squeeze(2) - assert masks.ndim == 4, ( - "Expected masks to be of shape(N_preds,T_frames,H,W)" - ) + assert ( + masks.ndim == 4 + ), "Expected masks to be of shape(N_preds,T_frames,H,W)" areas = [mask.flatten(1).sum(1).tolist() for mask in masks] rles = [rle_encode(masklet) for masklet in masks] diff --git a/sam3/logger.py b/sam3/logger.py index 2ce9c09..35dcc0d 100644 --- a/sam3/logger.py +++ b/sam3/logger.py @@ -42,9 +42,9 @@ def get_logger(name, level=logging.INFO): """A command line logger.""" if "LOG_LEVEL" in os.environ: level = os.environ["LOG_LEVEL"].upper() - assert level in LOG_LEVELS, ( - f"Invalid LOG_LEVEL: {level}, must be one of {list(LOG_LEVELS.keys())}" - ) + assert ( + level in LOG_LEVELS + ), f"Invalid LOG_LEVEL: {level}, must be one of {list(LOG_LEVELS.keys())}" level = LOG_LEVELS[level] logger = logging.getLogger(name) logger.setLevel(level) diff --git a/sam3/model/data_misc.py b/sam3/model/data_misc.py index 298340d..cc6a7bb 100644 --- a/sam3/model/data_misc.py +++ b/sam3/model/data_misc.py @@ -16,6 +16,53 @@ MyTensor = Union[torch.Tensor, List[Any]] +class NestedTensor: + def __init__(self, tensors, mask): + self.tensors = tensors + self.mask = mask + + def to(self, *args, **kwargs): + cast_tensor = self.tensors.to(*args, **kwargs) + cast_mask = self.mask.to(*args, **kwargs) if self.mask is not None else None + return type(self)(cast_tensor, cast_mask) + + def clone(self): + new_tensors = self.tensors.clone() + new_mask = None if self.mask is None else self.mask.clone() + return NestedTensor(new_tensors, new_mask) + + def __getitem__(self, idx): + return self.tensors[idx] + + def __len__(self): + return len(self.tensors) + + @property + def device(self): + return self.tensors.device + + @property + def shape(self): + return self.tensors.shape + + # custom memory pinning method on custom type + def pin_memory(self, device=None): + self.tensors = self.tensors.pin_memory(device) + if self.mask is not None: + self.mask = self.mask.pin_memory(device) + + +# Register NestedTensor as a pytree node so tree_map_only can traverse into it +# (matches onevision/utils/misc.py registration) +from torch.utils import _pytree as pytree + +pytree.register_pytree_node( + NestedTensor, + lambda x: ([x.tensors, x.mask], None), + lambda values, _: NestedTensor(values[0], values[1]), +) + + def interpolate( input, size=None, scale_factor=None, mode="nearest", align_corners=None ): @@ -28,9 +75,9 @@ def interpolate( input, size, scale_factor, mode, align_corners ) - assert input.shape[0] != 0 or input.shape[1] != 0, ( - "At least one of the two first dimensions must be non zero" - ) + assert ( + input.shape[0] != 0 or input.shape[1] != 0 + ), "At least one of the two first dimensions must be non zero" if input.shape[1] == 0: # Pytorch doesn't support null dimension on the channel dimension, so we transpose to fake a null batch dim @@ -81,6 +128,15 @@ class FindStage: # This is beneficial for tracking in videos without the need for pointers. object_ids: Optional[List[List]] = None # List of objects per query + # Multiplex-specific fields (used by sam3_demo_multiplex) + img_ids_np: Optional[Any] = None + input_boxes_before_embed: Optional[MyTensor] = None + input_boxes_before_embed__type = torch.float + input_points_before_embed: Optional[MyTensor] = None + input_points_before_embed__type = torch.float + ptrs: Optional[Any] = None + ptrs_seg: Optional[Any] = None + @dataclass class BatchedFindTarget: @@ -165,6 +221,7 @@ class BatchedDatapoint: find_targets: List[BatchedFindTarget] find_metadatas: List[BatchedInferenceMetadata] raw_images: Optional[List[Any]] = None + get_queries: Optional[Any] = None def convert_my_tensors(obj): @@ -188,6 +245,7 @@ def is_optional_field(field) -> bool: ): stack_dim = 0 if field.name in [ + "input_boxes_before_embed", "input_boxes", "input_boxes_label", ]: diff --git a/sam3/model/decoder.py b/sam3/model/decoder.py index 7a204be..098e2a3 100644 --- a/sam3/model/decoder.py +++ b/sam3/model/decoder.py @@ -6,12 +6,17 @@ Inspired from Pytorch's version, adds the pre-norm variant """ -from typing import Any, Dict, List, Optional +import math +from functools import partial +from typing import Any, Dict, List, Optional, Union import numpy as np import torch +import torch.nn.functional as torchF +from sam3.sam.rope import apply_rotary_enc, apply_rotary_enc_real, compute_axial_cis from sam3.sam.transformer import RoPEAttention from torch import nn, Tensor +from torch.nn.attention import sdpa_kernel, SDPBackend from torchvision.ops.roi_align import RoIAlign from .act_ckpt_utils import activation_ckpt_wrapper @@ -439,9 +444,9 @@ def forward( - valid_ratios/spatial_shapes: bs, nlevel, 2 """ if memory_mask is not None: - assert self.boxRPB == "none", ( - "inputting a memory_mask in the presence of boxRPB is unexpected/not implemented" - ) + assert ( + self.boxRPB == "none" + ), "inputting a memory_mask in the presence of boxRPB is unexpected/not implemented" apply_dac = apply_dac if apply_dac is not None else self.dac if apply_dac: @@ -511,18 +516,18 @@ def forward( query_pos = self.ref_point_head(query_sine_embed) # nq, bs, d_model if self.boxRPB != "none" and reference_boxes is not None: - assert spatial_shapes.shape[0] == 1, ( - "only single scale support implemented" - ) + assert ( + spatial_shapes.shape[0] == 1 + ), "only single scale support implemented" memory_mask = self._get_rpb_matrix( reference_boxes, (spatial_shapes[0, 0], spatial_shapes[0, 1]), ) memory_mask = memory_mask.flatten(0, 1) # (bs*n_heads, nq, H*W) if self.training: - assert self.use_act_checkpoint, ( - "Activation checkpointing not enabled in the decoder" - ) + assert ( + self.use_act_checkpoint + ), "Activation checkpointing not enabled in the decoder" output, presence_out = activation_ckpt_wrapper(layer)( tgt=output, tgt_query_pos=query_pos, @@ -671,9 +676,9 @@ def forward( src_pos[0], ) - assert src.shape[1] == prompt.shape[1], ( - "Batch size must be the same for src and prompt" - ) + assert ( + src.shape[1] == prompt.shape[1] + ), "Batch size must be the same for src and prompt" output = src @@ -951,3 +956,419 @@ def forward(self, *args: Any, **kwds: Any) -> torch.Tensor: if self.pre_norm: return self.forward_pre(*args, **kwds) raise NotImplementedError + + +def functional_attention( + q: Tensor, + k: Tensor, + v: Tensor, + *, + dropout: float, + num_heads: int, + num_k_exclude_rope: int = 0, + freqs_cis: Optional[Tensor] = None, + freqs_cis_real: Optional[Tensor] = None, + freqs_cis_imag: Optional[Tensor] = None, + use_fa3: bool = False, + use_rope_real: bool = False, + rope_k_repeat: bool, +) -> Union[Tensor, tuple[Tensor, Tensor]]: + b, n, cq = q.shape + _, m, ck = k.shape + _, _, cv = v.shape + if b > 1: + assert k.shape[0] == v.shape[0] == b + else: + # broadcast-able + assert k.shape[0] == b == 1, f"{q.shape=} {k.shape=} {v.shape=}" + assert v.shape[1] == m + + q = q.reshape(b, n, num_heads, cq // num_heads).transpose(1, 2) + k = k.reshape(b, m, num_heads, ck // num_heads).transpose(1, 2) + v = v.reshape(v.shape[0], m, num_heads, cv // num_heads).transpose(1, 2) + + if freqs_cis is not None: + num_k_rope = k.size(-2) - num_k_exclude_rope + if use_rope_real: + q, k[:, :, :num_k_rope] = apply_rotary_enc_real( + q, + k[:, :, :num_k_rope], + freqs_cis_real=freqs_cis_real, + freqs_cis_imag=freqs_cis_imag, + repeat_freqs_k=rope_k_repeat, + ) + else: + q, k[:, :, :num_k_rope] = apply_rotary_enc( + q, + k[:, :, :num_k_rope], + freqs_cis, + repeat_freqs_k=rope_k_repeat, + ) + + if use_fa3: + from sam3.perflib.fa3 import flash_attn_func + + assert dropout == 0.0 + out = flash_attn_func(q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)) + else: + with sdpa_kernel(SDPBackend.FLASH_ATTENTION): + out = torchF.scaled_dot_product_attention(q, k, v, dropout_p=dropout) + out = out.transpose(1, 2) # B * n * n_heads * (cv // num_heads) + + out = out.reshape(b, n, cv) + return out + + +class SimpleRoPEAttention(nn.Module): + """ + Attention with rotary position encoding. + This class is "simple" because it does not perform q/k/v/out projections. + """ + + def __init__( + self, + d_model: int, + num_heads: int, + dropout_p: float, + rope_theta=10000.0, + # whether to repeat q rope to match k length + # this is needed for cross-attention to memories + rope_k_repeat=False, + feat_sizes=(64, 64), # [w, h] for stride 16 feats at 1024 resolution + use_fa3: bool = False, + use_rope_real: bool = False, + ): + super().__init__() + + self.num_heads = num_heads + self.dropout_p = dropout_p + self.compute_cis = partial( + compute_axial_cis, dim=d_model // num_heads, theta=rope_theta + ) + device = torch.device("cuda") if torch.cuda.is_available() else None + self.freqs_cis = self.compute_cis( + end_x=feat_sizes[0], end_y=feat_sizes[1], device=device + ) + + self.use_fa3 = use_fa3 + self.use_rope_real = use_rope_real + if self.use_rope_real: + self.freqs_cis_real = self.freqs_cis.real + self.freqs_cis_imag = self.freqs_cis.imag + self.rope_k_repeat = rope_k_repeat + + def forward( + self, + q: Tensor, + k: Tensor, + v: Tensor, + num_k_exclude_rope: int = 0, + ) -> Union[Tensor, tuple[Tensor, Tensor]]: + # Apply rotary position encoding + w = h = math.sqrt(q.shape[-2]) + self.freqs_cis = self.freqs_cis.to(q.device) + if self.freqs_cis.shape[0] != q.shape[-2]: + self.freqs_cis = self.compute_cis(end_x=w, end_y=h, device=q.device) + if self.use_rope_real: + self.freqs_cis_real = self.freqs_cis.real + self.freqs_cis_imag = self.freqs_cis.imag + if q.shape[-2] != k.shape[-2]: + assert self.rope_k_repeat + + dropout_p = self.dropout_p if self.training else 0.0 + out = functional_attention( + q, + k, + v, + dropout=dropout_p, + num_heads=self.num_heads, + num_k_exclude_rope=num_k_exclude_rope, + freqs_cis=self.freqs_cis, + freqs_cis_real=self.freqs_cis_real if self.use_rope_real else None, + freqs_cis_imag=self.freqs_cis_imag if self.use_rope_real else None, + use_fa3=self.use_fa3, + use_rope_real=self.use_rope_real, + rope_k_repeat=self.rope_k_repeat, + ) + + return out + + +class DecoupledTransformerDecoderLayerv2(nn.Module): + def __init__( + self, + *, + activation: str, + d_model: int, + num_heads: int, + dim_feedforward: int, + dropout: float, + pos_enc_at_attn: bool, + pos_enc_at_cross_attn_keys: bool, + pos_enc_at_cross_attn_queries: bool, + pre_norm: bool, + cross_attention_first: bool = False, + self_attention_rope: SimpleRoPEAttention, + cross_attention_rope: SimpleRoPEAttention, + ): + super().__init__() + self.d_model = d_model + self.num_heads = num_heads + self.dim_feedforward = dim_feedforward + self.dropout_value = dropout + + self.self_attn_q_proj = nn.Linear(d_model, d_model) + self.self_attn_k_proj = nn.Linear(d_model, d_model) + self.self_attn_v_proj = nn.Linear(d_model, d_model) + self.self_attn_out_proj = nn.Linear(d_model, d_model) + + self.cross_attn_q_proj = nn.Linear(d_model, d_model) + self.cross_attn_k_proj = nn.Linear(d_model, d_model) + self.cross_attn_v_proj = nn.Linear(d_model, d_model) + self.cross_attn_out_proj = nn.Linear(d_model, d_model) + + self.image_cross_attn_q_proj = nn.Linear(d_model, d_model) + self.image_cross_attn_k_proj = nn.Linear(d_model, d_model) + + self.self_attention_rope = self_attention_rope + self.cross_attention_rope = cross_attention_rope + + # Implementation of Feedforward model + self.linear1 = nn.Linear(d_model, dim_feedforward) + self.dropout = nn.Dropout(dropout) + self.linear2 = nn.Linear(dim_feedforward, d_model) + + self.norm1 = nn.LayerNorm(d_model) + self.norm2 = nn.LayerNorm(d_model) + self.norm3 = nn.LayerNorm(d_model) + self.dropout1 = nn.Dropout(dropout) + self.dropout2 = nn.Dropout(dropout) + self.dropout3 = nn.Dropout(dropout) + + self.activation_str = activation + self.activation = get_activation_fn(activation) + self.pre_norm = pre_norm + + self.pos_enc_at_attn = pos_enc_at_attn + self.pos_enc_at_cross_attn_queries = pos_enc_at_cross_attn_queries + self.pos_enc_at_cross_attn_keys = pos_enc_at_cross_attn_keys + + self.cross_attention_first = cross_attention_first + + def _forward_sa(self, tgt, query_pos): + # Self-Attention + tgt2 = self.norm1(tgt) + + q = k = tgt2 + query_pos if self.pos_enc_at_attn else tgt2 + + q = self.self_attn_q_proj(q) + k = self.self_attn_k_proj(k) + v = self.self_attn_v_proj(tgt2) + out = self.self_attention_rope(q, k, v) + tgt2 = self.self_attn_out_proj(out) + + tgt = tgt + self.dropout1(tgt2) + return tgt + + def _forward_ca( + self, + *, + image, + tgt, + memory_image, + memory, + query_pos, + memory_image_pos, + num_k_exclude_rope=0, + ): + kwds = {} + if num_k_exclude_rope > 0: + assert isinstance(self.cross_attention_rope, SimpleRoPEAttention) + kwds = {"num_k_exclude_rope": num_k_exclude_rope} + + # Cross-Attention + tgt2 = self.norm2(tgt) + + q = self.image_cross_attn_q_proj(image) + self.cross_attn_q_proj(tgt2) + if self.pos_enc_at_cross_attn_queries: + q = q + query_pos + k = self.image_cross_attn_k_proj(memory_image) + self.cross_attn_k_proj(memory) + if self.pos_enc_at_cross_attn_keys: + k = k + memory_image_pos + v = self.cross_attn_v_proj(memory) + + out = self.cross_attention_rope(q, k, v, **kwds) + tgt2 = self.cross_attn_out_proj(out) + + tgt = tgt + self.dropout2(tgt2) + return tgt + + def forward_pre( + self, + *, + image, + tgt, + memory_image, + memory, + image_pos: Optional[Tensor] = None, + query_pos: Optional[Tensor] = None, + memory_image_pos: Optional[Tensor] = None, + memory_pos: Optional[Tensor] = None, + num_k_exclude_rope: int = 0, + ): + if self.cross_attention_first: + tgt = self._forward_ca( + image=image, + tgt=tgt, + memory_image=memory_image, + memory=memory, + query_pos=query_pos, + memory_image_pos=memory_image_pos, + num_k_exclude_rope=num_k_exclude_rope, + ) + tgt = self._forward_sa(tgt, query_pos) + else: + tgt = self._forward_sa(tgt, query_pos) + tgt = self._forward_ca( + image=image, + tgt=tgt, + memory_image=memory_image, + memory=memory, + query_pos=query_pos, + memory_image_pos=memory_image_pos, + num_k_exclude_rope=num_k_exclude_rope, + ) + + # MLP + tgt2 = self.norm3(tgt) + tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt2)))) + tgt = tgt + self.dropout3(tgt2) + + return image, tgt + + def forward(self, *args: Any, **kwds: Any) -> torch.Tensor: + if self.pre_norm: + return self.forward_pre(*args, **kwds) + raise NotImplementedError + + +class TransformerEncoderDecoupledCrossAttention(nn.Module): + def __init__( + self, + d_model: int, + frozen: bool, + pos_enc_at_input: bool, + layer, + num_layers: int, + use_act_checkpoint: bool = False, + batch_first: bool = False, # Do layers expect batch first input? + use_image_in_output: bool = True, + ): + super().__init__() + self.d_model = d_model + self.layers = get_clones(layer, num_layers) + self.num_layers = num_layers + self.norm = nn.LayerNorm(d_model) + self.pos_enc_at_input = pos_enc_at_input + self.use_act_checkpoint = use_act_checkpoint + self.use_image_in_output = use_image_in_output + + if frozen: + for p in self.parameters(): + p.requires_grad_(False) + + self.batch_first = batch_first + + def forward( + self, + image: Tensor, # image features + src: Tensor, # self-attention inputs; object features + memory_image: Tensor, # cross-attention inputs; image features + memory: Tensor, # cross-attention inputs; object features + image_pos: Optional[Tensor] = None, # pos_enc for self-attention inputs + src_pos: Optional[Tensor] = None, # pos_enc for self-attention inputs + memory_image_pos: Optional[Tensor] = None, # pos_enc for cross-attention inputs + memory_pos: Optional[Tensor] = None, # pos_enc for cross-attention inputs + num_obj_ptr_tokens: int = 0, # number of object pointer *tokens* + ): + assert ( + src.shape[1] == memory.shape[1] + ), "Batch size must be the same for src and memory" + assert ( + image.shape[1] == memory_image.shape[1] + ), "Batch size must be the same for image and memory_image" + + output = src + + if self.pos_enc_at_input and src_pos is not None: + output = output + 0.1 * src_pos + + if self.batch_first: + # Convert to batch first + output = output.transpose(0, 1) + src_pos = src_pos.transpose(0, 1) + image = image.transpose(0, 1) + memory = memory.transpose(0, 1) + memory_pos = memory_pos.transpose(0, 1) + memory_image = memory_image.transpose(0, 1) + memory_image_pos = memory_image_pos.transpose(0, 1) + + if memory_image.shape[1] != memory.shape[1]: + # Pad memory_image with zeros, to accodmate object pointers + assert ( + (memory.shape[1] - memory_image.shape[1]) == num_obj_ptr_tokens + ), f"{memory.shape[1]} - {memory_image.shape[1]} != {num_obj_ptr_tokens}" + memory_image = torch.cat( + [ + memory_image, + torch.zeros( + (memory_image.shape[0], num_obj_ptr_tokens) + + memory_image.shape[2:], + dtype=memory_image.dtype, + device=memory_image.device, + ), + ], + dim=1, + ) + if memory_image_pos is not None: + assert ( + (memory_pos.shape[1] - memory_image_pos.shape[1]) + == num_obj_ptr_tokens + ), f"{memory_pos.shape[1]} - {memory_image_pos.shape[1]} != {num_obj_ptr_tokens}" + # tpos is the same in the batch anyway; note that memory_image always has a batch size of 1 + memory_image_pos = torch.cat( + [ + memory_image_pos, + memory_pos[0:1, -num_obj_ptr_tokens:], + ], + dim=1, + ) + + for layer in self.layers: + image, output = activation_ckpt_wrapper(layer)( + image=image, + tgt=output, + memory_image=memory_image, + memory=memory, + image_pos=image_pos, + query_pos=src_pos, + memory_image_pos=memory_image_pos, + memory_pos=memory_pos, + num_k_exclude_rope=num_obj_ptr_tokens, + act_ckpt_enable=self.training and self.use_act_checkpoint, + ) + + if self.use_image_in_output: + normed_output = self.norm(output + image) + else: + normed_output = self.norm(output) + + if self.batch_first: + # Convert back to seq first + normed_output = normed_output.transpose(0, 1) + src_pos = src_pos.transpose(0, 1) + + return { + "memory": normed_output, + "pos_embed": src_pos, + } diff --git a/sam3/model/encoder.py b/sam3/model/encoder.py index 3fc9406..d825df4 100644 --- a/sam3/model/encoder.py +++ b/sam3/model/encoder.py @@ -322,9 +322,9 @@ def get_reference_points(spatial_shapes, valid_ratios, device): return reference_points def _prepare_multilevel_features(self, srcs, masks, pos_embeds): - assert len(srcs) == self.num_feature_levels, ( - "mismatch between expected and received # of feature levels" - ) + assert ( + len(srcs) == self.num_feature_levels + ), "mismatch between expected and received # of feature levels" src_flatten = [] mask_flatten = [] @@ -406,9 +406,9 @@ def forward( - spatial_shapes: Spatial dimensions of each feature level - valid_ratios: Valid ratios for each feature level """ - assert len(src) == self.num_feature_levels, ( - "must be equal to num_feature_levels" - ) + assert ( + len(src) == self.num_feature_levels + ), "must be equal to num_feature_levels" if src_key_padding_masks is not None: assert len(src_key_padding_masks) == self.num_feature_levels if pos is not None: @@ -538,9 +538,9 @@ def forward( else None ) else: - assert all(x.dim == 4 for x in src), ( - "expected list of (bs, c, h, w) tensors" - ) + assert all( + x.dim == 4 for x in src + ), "expected list of (bs, c, h, w) tensors" if self.add_pooled_text_to_img_feat: # Fusion: Add mean pooled text to image features diff --git a/sam3/model/geometry_encoders.py b/sam3/model/geometry_encoders.py index d60ee54..35a4276 100644 --- a/sam3/model/geometry_encoders.py +++ b/sam3/model/geometry_encoders.py @@ -147,42 +147,54 @@ def __init__( ) # Dimension checks - assert box_embeddings is not None and list(box_embeddings.shape[:2]) == [ - box_seq_len, - bs, - ], ( - f"Wrong dimension for box embeddings. Expected [{box_seq_len}, {bs}, *] got {box_embeddings.shape}" - ) - assert box_mask is not None and list(box_mask.shape) == [ - bs, - box_seq_len, - ], ( - f"Wrong dimension for box mask. Expected [{bs}, {box_seq_len}] got {box_mask.shape}" - ) - assert point_embeddings is not None and list(point_embeddings.shape[:2]) == [ - point_seq_len, - bs, - ], ( - f"Wrong dimension for point embeddings. Expected [{point_seq_len}, {bs}, *] got {point_embeddings.shape}" - ) - assert point_mask is not None and list(point_mask.shape) == [ - bs, - point_seq_len, - ], ( - f"Wrong dimension for point mask. Expected [{bs}, {point_seq_len}] got {point_mask.shape}" - ) - assert box_labels is not None and list(box_labels.shape) == [ - box_seq_len, - bs, - ], ( - f"Wrong dimension for box labels. Expected [{box_seq_len}, {bs}] got {box_labels.shape}" - ) - assert point_labels is not None and list(point_labels.shape) == [ - point_seq_len, - bs, - ], ( - f"Wrong dimension for point labels. Expected [{point_seq_len}, {bs}] got {point_labels.shape}" - ) + assert ( + box_embeddings is not None + and list(box_embeddings.shape[:2]) + == [ + box_seq_len, + bs, + ] + ), f"Wrong dimension for box embeddings. Expected [{box_seq_len}, {bs}, *] got {box_embeddings.shape}" + assert ( + box_mask is not None + and list(box_mask.shape) + == [ + bs, + box_seq_len, + ] + ), f"Wrong dimension for box mask. Expected [{bs}, {box_seq_len}] got {box_mask.shape}" + assert ( + point_embeddings is not None + and list(point_embeddings.shape[:2]) + == [ + point_seq_len, + bs, + ] + ), f"Wrong dimension for point embeddings. Expected [{point_seq_len}, {bs}, *] got {point_embeddings.shape}" + assert ( + point_mask is not None + and list(point_mask.shape) + == [ + bs, + point_seq_len, + ] + ), f"Wrong dimension for point mask. Expected [{bs}, {point_seq_len}] got {point_mask.shape}" + assert ( + box_labels is not None + and list(box_labels.shape) + == [ + box_seq_len, + bs, + ] + ), f"Wrong dimension for box labels. Expected [{box_seq_len}, {bs}] got {box_labels.shape}" + assert ( + point_labels is not None + and list(point_labels.shape) + == [ + point_seq_len, + bs, + ] + ), f"Wrong dimension for point labels. Expected [{point_seq_len}, {bs}] got {point_labels.shape}" assert ( # Allowed to be None, we leave it to the encoder to check for validity before encoding. mask_embeddings is None @@ -191,41 +203,41 @@ def __init__( mask_seq_len, bs, ] - ), ( - f"Wrong dimension for mask embeddings. Expected [{mask_seq_len}, {bs}, *] got {mask_embeddings.shape}" - ) - assert mask_mask is None or list(mask_mask.shape) == [ - bs, - mask_seq_len, - ], ( - f"Wrong dimension for mask attn. mask. Expected [{bs}, {mask_seq_len}] got {mask_mask.shape}" - ) + ), f"Wrong dimension for mask embeddings. Expected [{mask_seq_len}, {bs}, *] got {mask_embeddings.shape}" + assert ( + mask_mask is None + or list(mask_mask.shape) + == [ + bs, + mask_seq_len, + ] + ), f"Wrong dimension for mask attn. mask. Expected [{bs}, {mask_seq_len}] got {mask_mask.shape}" # Device checks - assert box_embeddings is not None and box_embeddings.device == device, ( - f"Expected box embeddings to be on device {device}, got {box_embeddings.device}" - ) - assert box_mask is not None and box_mask.device == device, ( - f"Expected box mask to be on device {device}, got {box_mask.device}" - ) - assert box_labels is not None and box_labels.device == device, ( - f"Expected box labels to be on device {device}, got {box_labels.device}" - ) - assert point_embeddings is not None and point_embeddings.device == device, ( - f"Expected point embeddings to be on device {device}, got {point_embeddings.device}" - ) - assert point_mask is not None and point_mask.device == device, ( - f"Expected point mask to be on device {device}, got {point_mask.device}" - ) - assert point_labels is not None and point_labels.device == device, ( - f"Expected point labels to be on device {device}, got {point_labels.device}" - ) - assert mask_embeddings is None or mask_embeddings.device == device, ( - f"Expected mask embeddings to be on device {device}, got {mask_embeddings.device}" - ) - assert mask_mask is None or mask_mask.device == device, ( - f"Expected mask attn. mask to be on device {device}, got {mask_mask.device}" - ) + assert ( + box_embeddings is not None and box_embeddings.device == device + ), f"Expected box embeddings to be on device {device}, got {box_embeddings.device}" + assert ( + box_mask is not None and box_mask.device == device + ), f"Expected box mask to be on device {device}, got {box_mask.device}" + assert ( + box_labels is not None and box_labels.device == device + ), f"Expected box labels to be on device {device}, got {box_labels.device}" + assert ( + point_embeddings is not None and point_embeddings.device == device + ), f"Expected point embeddings to be on device {device}, got {point_embeddings.device}" + assert ( + point_mask is not None and point_mask.device == device + ), f"Expected point mask to be on device {device}, got {point_mask.device}" + assert ( + point_labels is not None and point_labels.device == device + ), f"Expected point labels to be on device {device}, got {point_labels.device}" + assert ( + mask_embeddings is None or mask_embeddings.device == device + ), f"Expected mask embeddings to be on device {device}, got {mask_embeddings.device}" + assert ( + mask_mask is None or mask_mask.device == device + ), f"Expected mask attn. mask to be on device {device}, got {mask_mask.device}" self.box_embeddings = box_embeddings self.point_embeddings = point_embeddings @@ -251,30 +263,30 @@ def _init_seq_len_and_device( if point_embeddings is not None: point_seq_len = point_embeddings.shape[0] if bs is not None: - assert bs == point_embeddings.shape[1], ( - f"Batch size mismatch between box and point embeddings. Got {bs} and {point_embeddings.shape[1]}." - ) + assert ( + bs == point_embeddings.shape[1] + ), f"Batch size mismatch between box and point embeddings. Got {bs} and {point_embeddings.shape[1]}." else: bs = point_embeddings.shape[1] if device is not None: - assert device == point_embeddings.device, ( - "Device mismatch between box and point embeddings" - ) + assert ( + device == point_embeddings.device + ), "Device mismatch between box and point embeddings" else: device = point_embeddings.device if mask_embeddings is not None: mask_seq_len = mask_embeddings.shape[0] if bs is not None: - assert bs == mask_embeddings.shape[1], ( - f"Batch size mismatch between box/point and mask embedding. Got {bs} and {mask_embeddings.shape[1]}" - ) + assert ( + bs == mask_embeddings.shape[1] + ), f"Batch size mismatch between box/point and mask embedding. Got {bs} and {mask_embeddings.shape[1]}" else: bs = mask_embeddings.shape[1] if device is not None: - assert device == mask_embeddings.device, ( - "Device mismatch between box/point and mask embeddings." - ) + assert ( + device == mask_embeddings.device + ), "Device mismatch between box/point and mask embeddings." else: device = mask_embeddings.device @@ -526,9 +538,9 @@ def __init__( if add_cls: self.cls_embed = torch.nn.Embedding(1, self.d_model) - assert points_direct_project or points_pos_enc or points_pool, ( - "Error: need at least one way to encode points" - ) + assert ( + points_direct_project or points_pos_enc or points_pool + ), "Error: need at least one way to encode points" assert ( encode_boxes_as_points or boxes_direct_project @@ -570,16 +582,16 @@ def __init__( self.encode = None if num_layers > 0: - assert add_cls, ( - "It's currently highly recommended to add a CLS when using a transformer" - ) + assert ( + add_cls + ), "It's currently highly recommended to add a CLS when using a transformer" self.encode = get_clones(layer, num_layers) self.encode_norm = nn.LayerNorm(self.d_model) if mask_encoder is not None: - assert isinstance(mask_encoder, MaskEncoder), ( - f"Expected mask_encoder of type MaskEncoder. Got {type(mask_encoder)}." - ) + assert isinstance( + mask_encoder, MaskEncoder + ), f"Expected mask_encoder of type MaskEncoder. Got {type(mask_encoder)}." if add_mask_label: self.mask_label_embed = torch.nn.Embedding(2, self.d_model) self.add_mask_label = add_mask_label @@ -688,15 +700,16 @@ def _encode_masks( img_feats: torch.Tensor = None, ): n_masks, bs = masks.shape[:2] - assert n_masks == 1, ( - "We assume one mask per prompt for now. Code should still be functional if this assertion is removed." - ) - assert list(attn_mask.shape) == [ - bs, - n_masks, - ], ( - f"Expected attn_mask to be of shape {bs}x{n_masks}. Got {list(attn_mask.shape)}." - ) + assert ( + n_masks == 1 + ), "We assume one mask per prompt for now. Code should still be functional if this assertion is removed." + assert ( + list(attn_mask.shape) + == [ + bs, + n_masks, + ] + ), f"Expected attn_mask to be of shape {bs}x{n_masks}. Got {list(attn_mask.shape)}." masks, pos = self.mask_encoder( masks=masks.flatten(0, 1).float(), pix_feat=img_feats, diff --git a/sam3/model/io_utils.py b/sam3/model/io_utils.py index 9d64c9c..470bb43 100644 --- a/sam3/model/io_utils.py +++ b/sam3/model/io_utils.py @@ -132,6 +132,13 @@ def load_video_frames( match = re.match(r"", video_path) num_frames = int(match.group(1)) if match else 60 return load_dummy_video(image_size, offload_video_to_cpu, num_frames=num_frames) + elif video_path.startswith(" where N is an integer + match = re.match(r"", video_path) + num_frames = int(match.group(1)) if match else 60 + return load_dummy_video( + image_size, offload_video_to_cpu, num_frames=num_frames, do_zeros=True + ) elif os.path.isdir(video_path): return load_video_frames_from_image_folder( image_folder=video_path, @@ -322,12 +329,15 @@ def load_video_frames_from_video_file_using_cv2( return video_tensor, original_height, original_width -def load_dummy_video(image_size, offload_video_to_cpu, num_frames=60): +def load_dummy_video(image_size, offload_video_to_cpu, num_frames=60, do_zeros=False): """ Load a dummy video with random frames for testing and compilation warmup purposes. """ video_height, video_width = 480, 640 # dummy original video sizes - images = torch.randn(num_frames, 3, image_size, image_size, dtype=torch.float16) + if not do_zeros: + images = torch.randn(num_frames, 3, image_size, image_size, dtype=torch.float16) + else: + images = torch.zeros(num_frames, 3, image_size, image_size, dtype=torch.float16) if not offload_video_to_cpu: images = images.cuda() return images, video_height, video_width diff --git a/sam3/model/maskformer_segmentation.py b/sam3/model/maskformer_segmentation.py index a2d5c68..4953b7c 100644 --- a/sam3/model/maskformer_segmentation.py +++ b/sam3/model/maskformer_segmentation.py @@ -107,6 +107,12 @@ def _embed_pixels( image_ids, encoder_hidden_states, ) -> torch.Tensor: + # Unwrap NestedTensors to plain tensors if needed (multiplex path) + from sam3.model.data_misc import NestedTensor + + def _unwrap(x): + return x.tensors if isinstance(x, NestedTensor) else x + feature_device = backbone_feats[0].device # features could be on CPU model_device = self.device image_ids_ = image_ids.to(feature_device) @@ -116,10 +122,14 @@ def _embed_pixels( backbone_visual_feats = [] for feat in backbone_feats: # Copy the img features per query (pixel decoder won't share img feats) - backbone_visual_feats.append(feat[image_ids_, ...].to(model_device)) + backbone_visual_feats.append( + _unwrap(feat)[image_ids_, ...].to(model_device) + ) else: # Bs=1, we rely on broadcasting for query-based processing - backbone_visual_feats = [bb_feat.clone() for bb_feat in backbone_feats] + backbone_visual_feats = [ + _unwrap(bb_feat).clone() for bb_feat in backbone_feats + ] # Extract visual embeddings encoder_hidden_states = encoder_hidden_states.permute(1, 2, 0) spatial_dim = math.prod(backbone_feats[-1].shape[-2:]) @@ -135,7 +145,7 @@ def _embed_pixels( else: pixel_embed = self.pixel_decoder(backbone_visual_feats) else: - backbone_feats = [x.to(model_device) for x in backbone_feats] + backbone_feats = [_unwrap(x).to(model_device) for x in backbone_feats] pixel_embed = self.pixel_decoder(backbone_feats) if pixel_embed.shape[0] == 1: # For batch_size=1 training, we can avoid the indexing to save memory @@ -248,9 +258,7 @@ def __init__( self.d_model = hidden_dim if dot_product_scorer is not None: - assert presence_head, ( - "Specifying a dot product scorer without a presence head is likely a mistake" - ) + assert presence_head, "Specifying a dot product scorer without a presence head is likely a mistake" self.presence_head = None if presence_head: diff --git a/sam3/model/memory.py b/sam3/model/memory.py index 196dbf9..4901cbe 100644 --- a/sam3/model/memory.py +++ b/sam3/model/memory.py @@ -38,14 +38,19 @@ def __init__( # Option to interpolate the input mask first before downsampling using convs. In that case, the total_stride is assumed to be after interpolation. # If set to input resolution or None, we don't interpolate. We default to None to be safe (for older configs or if not explicitly set) interpol_size=None, + # options for incorporating multiplex memory encoding + multiplex_count: int = 1, + starting_out_chan: int = 1, + input_channel_multiplier: int = 1, ): super().__init__() num_layers = int(math.log2(total_stride) // math.log2(stride)) + multiplex_count = multiplex_count * input_channel_multiplier assert stride**num_layers == total_stride self.encoder = nn.Sequential() - mask_in_chans, mask_out_chans = 1, 1 + mask_in_chans, mask_out_chans = multiplex_count, starting_out_chan for _ in range(num_layers): - mask_out_chans = mask_in_chans * (stride**2) + mask_out_chans = mask_out_chans * (stride**2) self.encoder.append( nn.Conv2d( mask_in_chans, @@ -60,11 +65,12 @@ def __init__( mask_in_chans = mask_out_chans self.encoder.append(nn.Conv2d(mask_out_chans, embed_dim, kernel_size=1)) + self.multiplex_count = multiplex_count self.interpol_size = interpol_size if self.interpol_size is not None: - assert isinstance(self.interpol_size, (list, tuple)), ( - f"Unsupported type {type(self.interpol_size)}. Should be a list or tuple." - ) + assert isinstance( + self.interpol_size, (list, tuple) + ), f"Unsupported type {type(self.interpol_size)}. Should be a list or tuple." self.interpol_size = list(interpol_size) assert len(self.interpol_size) == 2 diff --git a/sam3/model/model_misc.py b/sam3/model/model_misc.py index d961461..6c01ab3 100644 --- a/sam3/model/model_misc.py +++ b/sam3/model/model_misc.py @@ -6,18 +6,25 @@ import copy import math +import warnings import weakref from collections.abc import Iterator from contextlib import AbstractContextManager from enum import auto, Enum -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Tuple, Union import numpy as np import torch import torch.nn.functional as F from torch import nn, Tensor +from torch.overrides import handle_torch_function, has_torch_function from typing_extensions import override +try: + import xformers +except ImportError: + xformers = None + def inverse_sigmoid(x, eps=1e-3): """ @@ -30,10 +37,680 @@ def inverse_sigmoid(x, eps=1e-3): return torch.log(x1 / x2) -class MultiheadAttentionWrapper(nn.MultiheadAttention): - def forward(self, *args, **kwargs): - kwargs["need_weights"] = False - return super().forward(*args, **kwargs) +def get_sdpa_settings(): + if torch.cuda.is_available(): + old_gpu = torch.cuda.get_device_properties(0).major < 7 + # only use Flash Attention on Ampere (8.0) or newer GPUs + use_flash_attn = torch.cuda.get_device_properties(0).major >= 8 + if not use_flash_attn: + warnings.warn( + "Flash Attention is disabled as it requires a GPU with Ampere (8.0) CUDA capability.", + category=UserWarning, + stacklevel=2, + ) + # keep math kernel for PyTorch versions before 2.2 (Flash Attention v2 is only + # available on PyTorch 2.2+, while Flash Attention v1 cannot handle all cases) + pytorch_version = tuple(int(v) for v in torch.__version__.split(".")[:2]) + if pytorch_version < (2, 2): + warnings.warn( + f"You are using PyTorch {torch.__version__} without Flash Attention v2 support. " + "Consider upgrading to PyTorch 2.2+ for Flash Attention v2 (which could be faster).", + category=UserWarning, + stacklevel=2, + ) + math_kernel_on = pytorch_version < (2, 2) or not use_flash_attn + else: + old_gpu = True + use_flash_attn = False + math_kernel_on = True + + return old_gpu, use_flash_attn, math_kernel_on + + +OLD_GPU, USE_FLASH_ATTN, MATH_KERNEL_ON = get_sdpa_settings() + + +class AttentionType: + """Type of attention""" + + # Simple dot product attention + Vanilla = "Vanilla" + + # Efficient attention from xformers + Xformer = "Xformer" + + # Sparse attention + Sparse = "Sparse" + + # Deformable attention (not compatible with text) + Deformable = "Deformable" + + +def multi_head_attention_forward( + query: Tensor, + key: Tensor, + value: Tensor, + embed_dim_to_check: int, + num_heads: int, + in_proj_weight: Optional[Tensor], + in_proj_bias: Optional[Tensor], + bias_k: Optional[Tensor], + bias_v: Optional[Tensor], + add_zero_attn: bool, + dropout_p: float, + out_proj_weight: Tensor, + out_proj_bias: Optional[Tensor], + training: bool = True, + key_padding_mask: Optional[Tensor] = None, + need_weights: bool = True, + attn_mask: Optional[Tensor] = None, + use_separate_proj_weight: bool = False, + q_proj_weight: Optional[Tensor] = None, + k_proj_weight: Optional[Tensor] = None, + v_proj_weight: Optional[Tensor] = None, + static_k: Optional[Tensor] = None, + static_v: Optional[Tensor] = None, + average_attn_weights: bool = True, + is_causal: bool = False, + attn_type: AttentionType = AttentionType.Vanilla, + attn_sparsity: float = 0.0, + attn_bias: Optional[Tensor] = None, + use_fa3: bool = False, +) -> Tuple[Tensor, Optional[Tensor]]: + tens_ops = ( + query, + key, + value, + in_proj_weight, + in_proj_bias, + bias_k, + bias_v, + out_proj_weight, + out_proj_bias, + ) + if has_torch_function(tens_ops): + return handle_torch_function( + multi_head_attention_forward, + tens_ops, + query, + key, + value, + embed_dim_to_check, + num_heads, + in_proj_weight, + in_proj_bias, + bias_k, + bias_v, + add_zero_attn, + dropout_p, + out_proj_weight, + out_proj_bias, + training=training, + key_padding_mask=key_padding_mask, + need_weights=need_weights, + attn_mask=attn_mask, + is_causal=is_causal, + use_separate_proj_weight=use_separate_proj_weight, + q_proj_weight=q_proj_weight, + k_proj_weight=k_proj_weight, + v_proj_weight=v_proj_weight, + static_k=static_k, + static_v=static_v, + average_attn_weights=average_attn_weights, + use_fa3=use_fa3, + ) + + is_batched = True + + if is_causal: + raise NotImplementedError("is_causal is not supported in this implem") + attn_mask = None + + if not is_batched: + query = query.unsqueeze(1) + key = key.unsqueeze(1) + value = value.unsqueeze(1) + if key_padding_mask is not None: + key_padding_mask = key_padding_mask.unsqueeze(0) + + # set up shape vars + tgt_len, bsz, embed_dim = query.shape + src_len, _, _ = key.shape + if key_padding_mask is not None: + _kpm_dtype = key_padding_mask.dtype + if _kpm_dtype != torch.bool and not torch.is_floating_point(key_padding_mask): + raise AssertionError( + "only bool and floating types of key_padding_mask are supported" + ) + assert ( + embed_dim == embed_dim_to_check + ), f"was expecting embedding dimension of {embed_dim_to_check}, but got {embed_dim}" + if isinstance(embed_dim, torch.Tensor): + head_dim = embed_dim.div(num_heads, rounding_mode="trunc") + else: + head_dim = embed_dim // num_heads + assert ( + head_dim * num_heads == embed_dim + ), f"embed_dim {embed_dim} not divisible by num_heads {num_heads}" + if use_separate_proj_weight: + assert ( + key.shape[:2] == value.shape[:2] + ), f"key's sequence and batch dims {key.shape[:2]} do not match value's {value.shape[:2]}" + else: + assert ( + key.shape == value.shape + ), f"key shape {key.shape} does not match value shape {value.shape}" + + # + # compute in-projection + # + if not use_separate_proj_weight: + assert ( + in_proj_weight is not None + ), "use_separate_proj_weight is False but in_proj_weight is None" + q, k, v = F._in_projection_packed( + query, key, value, in_proj_weight, in_proj_bias + ) + else: + assert ( + q_proj_weight is not None + ), "use_separate_proj_weight is True but q_proj_weight is None" + assert ( + k_proj_weight is not None + ), "use_separate_proj_weight is True but k_proj_weight is None" + assert ( + v_proj_weight is not None + ), "use_separate_proj_weight is True but v_proj_weight is None" + if in_proj_bias is None: + b_q = b_k = b_v = None + else: + b_q, b_k, b_v = in_proj_bias.chunk(3) + q, k, v = F._in_projection( + query, + key, + value, + q_proj_weight, + k_proj_weight, + v_proj_weight, + b_q, + b_k, + b_v, + ) + + # prep attention mask + if attn_mask is not None: + if attn_mask.dtype == torch.uint8: + warnings.warn( + "Byte tensor for attn_mask in nn.MultiheadAttention is deprecated. Use bool tensor instead." + ) + attn_mask = attn_mask.to(torch.bool) + else: + assert ( + attn_mask.is_floating_point() or attn_mask.dtype == torch.bool + ), f"Only float, byte, and bool types are supported for attn_mask, not {attn_mask.dtype}" + # ensure attn_mask's dim is 3 + if attn_mask.dim() == 2: + correct_2d_size = (tgt_len, src_len) + if attn_mask.shape != correct_2d_size: + raise RuntimeError( + f"The shape of the 2D attn_mask is {attn_mask.shape}, but should be {correct_2d_size}." + ) + attn_mask = attn_mask.unsqueeze(0) + elif attn_mask.dim() == 3: + correct_3d_size = (bsz * num_heads, tgt_len, src_len) + if attn_mask.shape != correct_3d_size: + raise RuntimeError( + f"The shape of the 3D attn_mask is {attn_mask.shape}, but should be {correct_3d_size}." + ) + else: + raise RuntimeError( + f"attn_mask's dimension {attn_mask.dim()} is not supported" + ) + + # add bias along batch dimension (currently second) + if bias_k is not None and bias_v is not None: + assert static_k is None, "bias cannot be added to static key." + assert static_v is None, "bias cannot be added to static value." + k = torch.cat([k, bias_k.repeat(1, bsz, 1)]) + v = torch.cat([v, bias_v.repeat(1, bsz, 1)]) + if attn_mask is not None: + attn_mask = F.pad(attn_mask, (0, 1)) + if key_padding_mask is not None: + key_padding_mask = F.pad(key_padding_mask, (0, 1)) + else: + assert bias_k is None + assert bias_v is None + + # + # reshape q, k, v for multihead attention and make em batch first + # + q = q.contiguous().view(tgt_len, bsz * num_heads, head_dim).transpose(0, 1) + if static_k is None: + k = k.contiguous().view(k.shape[0], bsz * num_heads, head_dim).transpose(0, 1) + else: + assert ( + static_k.size(0) == bsz * num_heads + ), f"expecting static_k.size(0) of {bsz * num_heads}, but got {static_k.size(0)}" + assert ( + static_k.size(2) == head_dim + ), f"expecting static_k.size(2) of {head_dim}, but got {static_k.size(2)}" + k = static_k + if static_v is None: + v = v.contiguous().view(v.shape[0], bsz * num_heads, head_dim).transpose(0, 1) + else: + assert ( + static_v.size(0) == bsz * num_heads + ), f"expecting static_v.size(0) of {bsz * num_heads}, but got {static_v.size(0)}" + assert ( + static_v.size(2) == head_dim + ), f"expecting static_v.size(2) of {head_dim}, but got {static_v.size(2)}" + v = static_v + + # add zero attention along batch dimension (now first) + if add_zero_attn: + zero_attn_shape = (bsz * num_heads, 1, head_dim) + k = torch.cat( + [k, torch.zeros(zero_attn_shape, dtype=k.dtype, device=k.device)], dim=1 + ) + v = torch.cat( + [v, torch.zeros(zero_attn_shape, dtype=v.dtype, device=v.device)], dim=1 + ) + if attn_mask is not None: + attn_mask = F.pad(attn_mask, (0, 1)) + if key_padding_mask is not None: + key_padding_mask = F.pad(key_padding_mask, (0, 1)) + + # update source sequence length after adjustments + src_len = k.size(1) + + # merge key padding and attention masks + if key_padding_mask is not None: + assert ( + key_padding_mask.shape + == ( + bsz, + src_len, + ) + ), f"expecting key_padding_mask shape of {(bsz, src_len)}, but got {key_padding_mask.shape}" + key_padding_mask = ( + key_padding_mask.view(bsz, 1, 1, src_len) + .expand(-1, num_heads, -1, -1) + .reshape(bsz * num_heads, 1, src_len) + ) + if attn_mask is None: + attn_mask = key_padding_mask + elif attn_mask.dtype == torch.bool: + attn_mask = attn_mask.logical_or(key_padding_mask) + else: + attn_mask = attn_mask.masked_fill(key_padding_mask, float("-inf")) + + # convert mask to float + if attn_mask is not None and attn_mask.dtype == torch.bool: + new_attn_mask = torch.zeros_like(attn_mask, dtype=q.dtype) + new_attn_mask.masked_fill_(attn_mask, float("-inf")) + attn_mask = new_attn_mask + + # adjust dropout probability + if not training: + dropout_p = 0.0 + + # + # (deep breath) calculate attention and out projection + # + + if attn_mask is not None: + if attn_mask.size(0) == 1: + attn_mask = attn_mask.unsqueeze(0) + else: + attn_mask = attn_mask.view(bsz, num_heads, -1, src_len) + + if attn_bias is not None: + assert ( + attn_bias.shape + == ( + bsz, + num_heads, + tgt_len, + src_len, + ) + ), f"expecting attn_bias shape of {(bsz, num_heads, tgt_len, src_len)}, but got {attn_bias.shape}" + if attn_mask is None: + attn_mask = attn_bias + else: + attn_mask = attn_mask + attn_bias + + q = q.view(bsz, num_heads, tgt_len, head_dim) + k = k.view(bsz, num_heads, src_len, head_dim) + v = v.view(bsz, num_heads, src_len, head_dim) + + if attn_type == AttentionType.Vanilla: + if attn_mask is None and not is_causal and use_fa3: + from sam3.perflib.fa3 import flash_attn_func + + assert dropout_p == 0.0 + attn_output = flash_attn_func( + q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2) + ).transpose(1, 2) + else: + torch.backends.cuda.enable_flash_sdp(True) + torch.backends.cuda.enable_math_sdp(True) + torch.backends.cuda.enable_mem_efficient_sdp(True) + + attn_output = F.scaled_dot_product_attention( + q, k, v, attn_mask, dropout_p, is_causal + ) + + attn_output = ( + attn_output.permute(2, 0, 1, 3).contiguous().view(bsz * tgt_len, embed_dim) + ) + elif attn_type == AttentionType.Xformer: + attn_output_weights = None + assert not need_weights, "need_weights is not supported in efficient mode" + attn_output = xformers.ops.memory_efficient_attention( + q.transpose(1, 2), + k.transpose(1, 2), + v.transpose(1, 2), + attn_bias=attn_mask, + p=dropout_p, + ) + attn_output = attn_output.permute(1, 0, 2, 3).reshape(bsz * tgt_len, embed_dim) + elif attn_type == AttentionType.Sparse: + attn_output_weights = None + assert not need_weights, "need_weights is not supported in efficient mode" + # Need to collapse heads and batch dimensions + q = q.reshape(bsz * num_heads, tgt_len, head_dim).contiguous() + k = k.reshape(bsz * num_heads, src_len, head_dim).contiguous() + v = v.reshape(bsz * num_heads, src_len, head_dim).contiguous() + row_offsets, column_indices = xformers.ops.find_locations_new( + q, k, attn_sparsity, True + ) + attn_output = xformers.ops.sparse_memory_efficient_attention( + q, k, v, row_offsets, column_indices, attn_bias=attn_mask + ).reshape(bsz, num_heads, tgt_len, head_dim) + attn_output = attn_output.permute(2, 0, 1, 3).reshape(bsz * tgt_len, embed_dim) + else: + raise ValueError(f"Unsupported attention type {attn_type}") + + attn_output = F.linear(attn_output, out_proj_weight, out_proj_bias) + attn_output = attn_output.view(tgt_len, bsz, attn_output.size(1)) + + if need_weights: + attn_output_weights = (q * head_dim**-0.5) @ k.transpose(-2, -1) + attn_output_weights = attn_output_weights.softmax(dim=-1) + attn_output_weights = attn_output_weights.view(bsz, num_heads, tgt_len, src_len) + if average_attn_weights: + attn_output_weights = attn_output_weights.sum(dim=1) / num_heads + + if not is_batched: + attn_output = attn_output.squeeze(1) + attn_output_weights = attn_output_weights.squeeze(0) + return attn_output, attn_output_weights + else: + attn_output_weights = None + if not is_batched: + attn_output = attn_output.squeeze(1) + return attn_output, None + + +class MultiheadAttention(nn.Module): + __constants__ = ["batch_first"] + bias_k: Optional[torch.Tensor] + bias_v: Optional[torch.Tensor] + + def __init__( + self, + embed_dim, + num_heads, + dropout=0.0, + bias=True, + add_bias_kv=False, + add_zero_attn=False, + kdim=None, + vdim=None, + batch_first=False, + device=None, + dtype=None, + attn_type: AttentionType = AttentionType.Vanilla, + sparsity: float = 0.0, + use_act_checkpoint: bool = False, + use_fa3: bool = False, + ) -> None: + factory_kwargs = {"device": device, "dtype": dtype} + super(MultiheadAttention, self).__init__() + self.embed_dim = embed_dim + self.kdim = kdim if kdim is not None else embed_dim + self.vdim = vdim if vdim is not None else embed_dim + self._qkv_same_embed_dim = self.kdim == embed_dim and self.vdim == embed_dim + + self.num_heads = num_heads + self.dropout = dropout + self.batch_first = batch_first + self.head_dim = embed_dim // num_heads + self.use_act_checkpoint = use_act_checkpoint + assert ( + self.head_dim * num_heads == self.embed_dim + ), "embed_dim must be divisible by num_heads" + + assert ( + attn_type == AttentionType.Sparse or sparsity == 0.0 + ), "sparsity is only supported for sparse attention" + + if not self._qkv_same_embed_dim: + self.q_proj_weight = nn.Parameter( + torch.empty((embed_dim, embed_dim), **factory_kwargs) + ) + self.k_proj_weight = nn.Parameter( + torch.empty((embed_dim, self.kdim), **factory_kwargs) + ) + self.v_proj_weight = nn.Parameter( + torch.empty((embed_dim, self.vdim), **factory_kwargs) + ) + self.register_parameter("in_proj_weight", None) + else: + self.in_proj_weight = nn.Parameter( + torch.empty((3 * embed_dim, embed_dim), **factory_kwargs) + ) + self.register_parameter("q_proj_weight", None) + self.register_parameter("k_proj_weight", None) + self.register_parameter("v_proj_weight", None) + + if bias: + self.in_proj_bias = nn.Parameter( + torch.empty(3 * embed_dim, **factory_kwargs) + ) + else: + self.register_parameter("in_proj_bias", None) + self.out_proj = nn.modules.linear.NonDynamicallyQuantizableLinear( + embed_dim, embed_dim, bias=bias, **factory_kwargs + ) + + if add_bias_kv: + self.bias_k = nn.Parameter(torch.empty((1, 1, embed_dim), **factory_kwargs)) + self.bias_v = nn.Parameter(torch.empty((1, 1, embed_dim), **factory_kwargs)) + else: + self.bias_k = self.bias_v = None + + self.add_zero_attn = add_zero_attn + + self.attn_type = attn_type + self.sparsity = sparsity + self.use_fa3 = use_fa3 + + self._reset_parameters() + + def _reset_parameters(self): + if self._qkv_same_embed_dim: + nn.init.xavier_uniform_(self.in_proj_weight) + else: + nn.init.xavier_uniform_(self.q_proj_weight) + nn.init.xavier_uniform_(self.k_proj_weight) + nn.init.xavier_uniform_(self.v_proj_weight) + + if self.in_proj_bias is not None: + nn.init.constant_(self.in_proj_bias, 0.0) + nn.init.constant_(self.out_proj.bias, 0.0) + if self.bias_k is not None: + nn.init.xavier_normal_(self.bias_k) + if self.bias_v is not None: + nn.init.xavier_normal_(self.bias_v) + + def __setstate__(self, state): + if "_qkv_same_embed_dim" not in state: + state["_qkv_same_embed_dim"] = True + + super(MultiheadAttention, self).__setstate__(state) + + def forward( + self, + query: Tensor, + key: Tensor, + value: Tensor, + key_padding_mask: Optional[Tensor] = None, + need_weights: bool = False, + attn_mask: Optional[Tensor] = None, + average_attn_weights: bool = True, + attn_bias: Optional[Tensor] = None, + ) -> Tuple[Tensor, Optional[Tensor]]: + is_batched = query.dim() == 3 + if key_padding_mask is not None: + _kpm_dtype = key_padding_mask.dtype + if _kpm_dtype != torch.bool and not torch.is_floating_point( + key_padding_mask + ): + raise AssertionError( + "only bool and floating types of key_padding_mask are supported" + ) + + if self.batch_first and is_batched: + if key is value: + if query is key: + query = key = value = query.transpose(1, 0) + else: + query, key = [x.transpose(1, 0) for x in (query, key)] + value = key + else: + query, key, value = [x.transpose(1, 0) for x in (query, key, value)] + + if not self._qkv_same_embed_dim: + if self.use_act_checkpoint: + attn_output, attn_output_weights = torch.utils.checkpoint.checkpoint( + multi_head_attention_forward, + query, + key, + value, + self.embed_dim, + self.num_heads, + self.in_proj_weight, + self.in_proj_bias, + self.bias_k, + self.bias_v, + self.add_zero_attn, + self.dropout, + self.out_proj.weight, + self.out_proj.bias, + use_reentrant=False, + training=self.training, + key_padding_mask=key_padding_mask, + need_weights=need_weights, + attn_mask=attn_mask, + use_separate_proj_weight=True, + q_proj_weight=self.q_proj_weight, + k_proj_weight=self.k_proj_weight, + v_proj_weight=self.v_proj_weight, + average_attn_weights=average_attn_weights, + attn_type=self.attn_type, + attn_sparsity=self.sparsity, + attn_bias=attn_bias, + use_fa3=self.use_fa3, + ) + else: + attn_output, attn_output_weights = multi_head_attention_forward( + query, + key, + value, + self.embed_dim, + self.num_heads, + self.in_proj_weight, + self.in_proj_bias, + self.bias_k, + self.bias_v, + self.add_zero_attn, + self.dropout, + self.out_proj.weight, + self.out_proj.bias, + training=self.training, + key_padding_mask=key_padding_mask, + need_weights=need_weights, + attn_mask=attn_mask, + use_separate_proj_weight=True, + q_proj_weight=self.q_proj_weight, + k_proj_weight=self.k_proj_weight, + v_proj_weight=self.v_proj_weight, + average_attn_weights=average_attn_weights, + attn_type=self.attn_type, + attn_sparsity=self.sparsity, + attn_bias=attn_bias, + use_fa3=self.use_fa3, + ) + else: + if self.use_act_checkpoint: + attn_output, attn_output_weights = torch.utils.checkpoint.checkpoint( + multi_head_attention_forward, + query, + key, + value, + self.embed_dim, + self.num_heads, + self.in_proj_weight, + self.in_proj_bias, + self.bias_k, + self.bias_v, + self.add_zero_attn, + self.dropout, + self.out_proj.weight, + self.out_proj.bias, + use_reentrant=False, + training=self.training, + key_padding_mask=key_padding_mask, + need_weights=need_weights, + attn_mask=attn_mask, + average_attn_weights=average_attn_weights, + attn_type=self.attn_type, + attn_sparsity=self.sparsity, + attn_bias=attn_bias, + ) + else: + attn_output, attn_output_weights = multi_head_attention_forward( + query, + key, + value, + self.embed_dim, + self.num_heads, + self.in_proj_weight, + self.in_proj_bias, + self.bias_k, + self.bias_v, + self.add_zero_attn, + self.dropout, + self.out_proj.weight, + self.out_proj.bias, + training=self.training, + key_padding_mask=key_padding_mask, + need_weights=need_weights, + attn_mask=attn_mask, + average_attn_weights=average_attn_weights, + attn_type=self.attn_type, + attn_sparsity=self.sparsity, + attn_bias=attn_bias, + ) + if self.batch_first and is_batched: + return attn_output.transpose(1, 0), attn_output_weights + else: + return attn_output, attn_output_weights + + +# Keep backward compatibility alias +MultiheadAttentionWrapper = MultiheadAttention class DotProductScoring(torch.nn.Module): @@ -330,9 +1007,9 @@ def __init__( self.output = output else: self.output = [] - assert isinstance(iter_mode, SAM3Output.IterMode), ( - f"iter_mode shoulf be of enum type 'SAM3Output.IterMode'. Got {type(iter_mode)}" - ) + assert isinstance( + iter_mode, SAM3Output.IterMode + ), f"iter_mode shoulf be of enum type 'SAM3Output.IterMode'. Got {type(iter_mode)}" self.iter_mode = iter_mode # We create a weak reference to self to be used in the lambda functions. @@ -411,9 +1088,9 @@ def iteration_mode( return SAM3Output._IterationMode(model_output=model_output, iter_mode=iter_mode) def append(self, item: list): - assert isinstance(item, list), ( - f"Only list items are supported. Got {type(item)}" - ) + assert isinstance( + item, list + ), f"Only list items are supported. Got {type(item)}" self.output.append(item) def __repr__(self): diff --git a/sam3/model/multiplex_mask_decoder.py b/sam3/model/multiplex_mask_decoder.py new file mode 100644 index 0000000..c584675 --- /dev/null +++ b/sam3/model/multiplex_mask_decoder.py @@ -0,0 +1,470 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + + +from typing import List, Optional, Type + +import torch +from sam3.sam.common import LayerNorm2d +from torch import nn +from torch.nn import functional as F + + +class MultiplexMaskDecoder(nn.Module): + def __init__( + self, + *, + transformer_dim: int, + transformer: nn.Module, + multiplex_count: int, + num_multimask_outputs: int = 3, + activation: Type[nn.Module] = nn.GELU, + iou_head_depth: int = 3, + iou_head_hidden_dim: int = 256, + use_high_res_features: bool = False, + iou_prediction_use_sigmoid: bool = False, + dynamic_multimask_via_stability=False, + dynamic_multimask_stability_delta=0.05, + dynamic_multimask_stability_thresh=0.98, + pred_obj_scores: bool = False, + pred_obj_scores_mlp: bool = False, + use_multimask_token_for_obj_ptr: bool = False, + decode_mask_with_shared_tokens: bool = False, + decode_mask_attribute_with_shared_tokens: bool = False, + multimask_outputs_only: bool = False, + ) -> None: + """ + Predicts masks given an image and prompt embeddings, using a + transformer architecture with multiplex capabilities. + + Arguments: + multiplex_count: the number of masks multiplexed into a single feature map + num_multimask_outputs: the number of masks to predict per multiplex output + (the total number of masks is (num_multimask_outputs+1) * multiplex_count) + use_multimask_token_for_obj_ptr: whether to use multimask tokens for object pointers + decode_mask_with_shared_tokens: use the same mask token for multimasks with different projection layers + decode_mask_attribute_with_shared_tokens: use the mask tokens (instead of separate tokens) + to predict iou and object scores + multimask_outputs_only: predict num_multimask_outputs masks without the single + mask output token (i.e., without the +1) + """ + super().__init__() + self.transformer_dim = transformer_dim + self.transformer = transformer + + self.multiplex_count = multiplex_count + self.num_multimask_outputs = num_multimask_outputs + self.multimask_outputs_only = multimask_outputs_only + self.decode_mask_with_shared_tokens = decode_mask_with_shared_tokens + self.decode_mask_attribute_with_shared_tokens = ( + decode_mask_attribute_with_shared_tokens + ) + + if self.decode_mask_with_shared_tokens: + assert ( + multimask_outputs_only + ), "multimask_outputs_only must be True if decode_mask_with_shared_tokens" + + if self.multimask_outputs_only: + self.num_mask_output_per_object = num_multimask_outputs + else: + self.num_mask_output_per_object = num_multimask_outputs + 1 + + if self.decode_mask_with_shared_tokens: + self.num_mask_tokens = multiplex_count + else: + self.num_mask_tokens = multiplex_count * self.num_mask_output_per_object + + self.pred_obj_scores = pred_obj_scores + self.use_multimask_token_for_obj_ptr = use_multimask_token_for_obj_ptr + + if not self.decode_mask_attribute_with_shared_tokens: + self.iou_token = nn.Embedding(multiplex_count, transformer_dim) + if self.pred_obj_scores: + self.obj_score_token = nn.Embedding(multiplex_count, transformer_dim) + + self.mask_tokens = nn.Embedding(self.num_mask_tokens, transformer_dim) + + self.output_upscaling = nn.Sequential( + nn.ConvTranspose2d( + transformer_dim, transformer_dim // 4, kernel_size=2, stride=2 + ), + LayerNorm2d(transformer_dim // 4), + activation(), + nn.ConvTranspose2d( + transformer_dim // 4, transformer_dim // 8, kernel_size=2, stride=2 + ), + activation(), + ) + self.use_high_res_features = use_high_res_features + if use_high_res_features: + self.conv_s0 = nn.Conv2d( + transformer_dim, transformer_dim // 8, kernel_size=1, stride=1 + ) + self.conv_s1 = nn.Conv2d( + transformer_dim, transformer_dim // 4, kernel_size=1, stride=1 + ) + + if self.num_multimask_outputs == 0: + self.output_hypernetworks_mlp = MLP( + transformer_dim, transformer_dim, transformer_dim // 8, 3 + ) + else: + self.output_hypernetworks_mlps = nn.ModuleList( + [ + MLP(transformer_dim, transformer_dim, transformer_dim // 8, 3) + for _ in range(self.num_mask_output_per_object) + ] + ) + + self.iou_prediction_head = MLP( + transformer_dim, + iou_head_hidden_dim, + ( + 1 + if ( + self.decode_mask_attribute_with_shared_tokens + and not self.decode_mask_with_shared_tokens + ) + else self.num_mask_output_per_object + ), + iou_head_depth, + sigmoid_output=iou_prediction_use_sigmoid, + ) + + if self.pred_obj_scores: + self.pred_obj_score_head = nn.Linear(transformer_dim, 1) + if pred_obj_scores_mlp: + self.pred_obj_score_head = MLP(transformer_dim, transformer_dim, 1, 3) + + # When outputting a single mask, optionally we can dynamically fall back to the best + # multimask output token if the single mask output token gives low stability scores. + self.dynamic_multimask_via_stability = dynamic_multimask_via_stability + self.dynamic_multimask_stability_delta = dynamic_multimask_stability_delta + self.dynamic_multimask_stability_thresh = dynamic_multimask_stability_thresh + + def forward( + self, + image_embeddings: torch.Tensor, + image_pe: torch.Tensor, + multimask_output: bool, + high_res_features: Optional[List[torch.Tensor]] = None, + extra_per_object_embeddings: Optional[torch.Tensor] = None, + ) -> dict[str, torch.Tensor]: + """ + Predict masks given image and prompt embeddings. + + Arguments: + image_embeddings (torch.Tensor): the embeddings from the image encoder + image_pe (torch.Tensor): positional encoding with the shape of image_embeddings + extra_per_object_embeddings (torch.Tensor): a tensor with shape b * multiplex_count * C to be added to the mask tokens + + Returns: a dict of Tensors indexed by strings + masks: batched predicted masks + iou_pred: batched predictions of mask quality + object_score_logits: batched predictions of object existence + """ + + if self.num_multimask_outputs <= 0: + assert ( + not multimask_output + ), f"multimask_output must be False with {self.num_multimask_outputs=}" + + if self.multimask_outputs_only: + assert ( + multimask_output + ), f"multimask_output must be True with {self.multimask_outputs_only=}" + + out = self.predict_masks( + image_embeddings=image_embeddings, + image_pe=image_pe, + high_res_features=high_res_features, + extra_per_object_embeddings=extra_per_object_embeddings, + ) + + masks = out["masks"] # [B, M, (self.num_mask_token_per_object), H, W] + iou_pred = out["iou_pred"] # [B, M, (self.num_mask_token_per_object)] + mask_tokens_out = out[ + "mask_tokens_out" + ] # [B, M, (self.num_mask_token_per_object), C] + + # Select the correct mask or masks for output + if multimask_output: + if not self.multimask_outputs_only: + masks = masks[:, :, 1:, :, :] + iou_pred = iou_pred[:, :, 1:] + elif self.dynamic_multimask_via_stability and not self.training: + masks, iou_pred = self._dynamic_multimask_via_stability(masks, iou_pred) + else: + masks = masks[:, :, 0:1, :, :] + iou_pred = iou_pred[:, :, 0:1] + + if multimask_output and self.use_multimask_token_for_obj_ptr: + if self.multimask_outputs_only: + sam_tokens_out = mask_tokens_out + else: + sam_tokens_out = mask_tokens_out[ + :, :, 1: + ] # [B, M, num_multimask_outputs, C] shape + else: + # Take the mask output token. Here we *always* use the token for single mask output. + # At test time, even if we track after 1-click (and using multimask_output=True), + # we still take the single mask token here. The rationale is that we always track + # after multiple clicks during training, so the past tokens seen during training + # are always the single mask token (and we'll let it be the object-memory token). + sam_tokens_out = mask_tokens_out[:, :, 0:1] # [B, M, 1, C] shape + + del out["mask_tokens_out"] + out["masks"] = masks + out["iou_pred"] = iou_pred + out["sam_tokens_out"] = sam_tokens_out + + if multimask_output: + assert ( + masks.shape[2] == self.num_mask_output_per_object + ), f"{masks.shape=}, {self.num_mask_output_per_object=}" + assert ( + iou_pred.shape[2] == self.num_mask_output_per_object + ), f"{iou_pred.shape=}, {self.num_mask_output_per_object=}" + if self.use_multimask_token_for_obj_ptr: + if self.decode_mask_with_shared_tokens: + assert sam_tokens_out.shape[2] == 1, f"{sam_tokens_out.shape=}" + else: + assert ( + sam_tokens_out.shape[2] == self.num_mask_output_per_object + ), f"{sam_tokens_out.shape=}, {self.num_mask_output_per_object=}" + else: + assert masks.shape[2] == 1, f"{masks.shape=}" + assert iou_pred.shape[2] == 1, f"{iou_pred.shape=}" + assert sam_tokens_out.shape[2] == 1, f"{sam_tokens_out.shape=}" + + return out + + def predict_masks( + self, + image_embeddings: torch.Tensor, + image_pe: torch.Tensor, + high_res_features: Optional[List[torch.Tensor]] = None, + extra_per_object_embeddings: Optional[ + torch.Tensor + ] = None, # num_buckets, multiplex_count, C + ) -> dict[str, torch.Tensor]: + """Predicts masks. See 'forward' for more details.""" + # Concatenate output tokens + B = image_embeddings.shape[0] + token_list = [] + if self.pred_obj_scores and not self.decode_mask_attribute_with_shared_tokens: + token_list.append(self.obj_score_token.weight) + if not self.decode_mask_attribute_with_shared_tokens: + token_list.append(self.iou_token.weight) + + tokens = torch.cat(token_list, dim=0) + tokens = tokens.unsqueeze(0).expand(B, -1, -1) + + if extra_per_object_embeddings is not None: + mask_tokens = self.mask_tokens.weight.view( + 1, self.multiplex_count, self.num_mask_output_per_object, -1 + ).expand(B, -1, -1, -1) + + mask_tokens = mask_tokens + extra_per_object_embeddings.unsqueeze(2) + mask_tokens = mask_tokens.flatten(1, 2) + else: + mask_tokens = self.mask_tokens.weight.unsqueeze(0).expand(B, -1, -1) + + tokens = torch.cat([tokens, mask_tokens], dim=1) + + src = image_embeddings + + assert ( + image_pe.size(0) == 1 + ), "image_pe should have size 1 in batch dim (from `get_dense_pe()`)" + pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0) + b, c, h, w = src.shape + + # Run the transformer + hs, src = self.transformer(src, pos_src, tokens) + + # Parse transformer outputs based on token sharing configuration + if self.decode_mask_attribute_with_shared_tokens: + assert ( + hs.shape[1] == self.num_mask_tokens + ), f"{hs.shape=}, {self.num_mask_tokens=}" + iou_token_out = mask_tokens_out = hs[:, 0 : self.num_mask_tokens] + if self.pred_obj_scores: + obj_score_token_out = mask_tokens_out + else: + # Separate tokens for each prediction type + s = 0 + if self.pred_obj_scores: + obj_score_token_out = hs[:, s : s + self.multiplex_count, :] + s += self.multiplex_count + + iou_token_out = hs[:, s : s + self.multiplex_count, :] + s += self.multiplex_count + mask_tokens_out = hs[:, s : s + self.num_mask_tokens, :] + assert ( + hs.shape[1] == s + self.num_mask_tokens + ), f"{hs.shape=}, {s=}, {self.num_mask_tokens=}" + + # Upscale mask embeddings and predict masks using the mask tokens + src = src.transpose(1, 2).view(b, c, h, w) + if not self.use_high_res_features: + upscaled_embedding = self.output_upscaling(src) + else: + dc1, ln1, act1, dc2, act2 = self.output_upscaling + feat_s0, feat_s1 = high_res_features + upscaled_embedding = act1(ln1(dc1(src) + feat_s1)) + upscaled_embedding = act2(dc2(upscaled_embedding) + feat_s0) + + if self.decode_mask_with_shared_tokens: + mask_tokens_out = mask_tokens_out.view(B, self.multiplex_count, 1, -1) + else: + mask_tokens_out = mask_tokens_out.view( + B, self.multiplex_count, self.num_mask_output_per_object, -1 + ) + if self.num_multimask_outputs == 0: + hyper_in = self.output_hypernetworks_mlp( + mask_tokens_out[:, :, 0, :] + ).unsqueeze(2) # [B, M, 1, C] + else: + hyper_in_list: List[torch.Tensor] = [] + for i in range(self.num_mask_output_per_object): + if self.decode_mask_with_shared_tokens: + hyper_in_list.append( + self.output_hypernetworks_mlps[i](mask_tokens_out[:, :, 0, :]) + ) + else: + hyper_in_list.append( + self.output_hypernetworks_mlps[i](mask_tokens_out[:, :, i, :]) + ) + # hyper_in: [B, M, num_multimask_outputs+1, C] + hyper_in = torch.stack(hyper_in_list, dim=2) + + # generate the masks + b, c, h, w = upscaled_embedding.shape + masks = torch.bmm( + hyper_in.flatten(1, 2), upscaled_embedding.view(b, c, h * w) + ).view(b, self.multiplex_count, self.num_mask_output_per_object, h, w) + + # Generate mask quality predictions, with shape b * multiplex_count * (num_multimask_outputs+1) + iou_pred = self.iou_prediction_head(iou_token_out).view( + b, self.multiplex_count, self.num_mask_output_per_object + ) + + if self.pred_obj_scores: + # Generate mask quality predictions, with shape b * (num_multimask_outputs+1) + if ( + self.decode_mask_attribute_with_shared_tokens + and not self.decode_mask_with_shared_tokens + ): + object_score_logits = ( + self.pred_obj_score_head(obj_score_token_out) + .view(b, self.multiplex_count, self.num_mask_output_per_object) + .sum(-1, keepdim=True) + ) + else: + object_score_logits = self.pred_obj_score_head(obj_score_token_out) + else: + # Obj scores logits - default to 10.0, i.e. assuming the object is present, sigmoid(10)=1 + object_score_logits = 10.0 * iou_pred.new_ones( + iou_pred.shape[0], iou_pred.shape[1] + ) + + outputs = { + "masks": masks, + "iou_pred": iou_pred, + "mask_tokens_out": mask_tokens_out, + "object_score_logits": object_score_logits, + } + + return outputs + + def _get_stability_scores(self, mask_logits): + """ + Compute stability scores of the mask logits based on the IoU between upper and + lower thresholds. + """ + mask_logits = mask_logits.flatten(-2) + stability_delta = self.dynamic_multimask_stability_delta + area_i = torch.sum(mask_logits > stability_delta, dim=-1).float() + area_u = torch.sum(mask_logits > -stability_delta, dim=-1).float() + stability_scores = torch.where(area_u > 0, area_i / area_u, 1.0) + return stability_scores + + def _dynamic_multimask_via_stability(self, all_mask_logits, all_iou_scores): + """ + When outputting a single mask, if the stability score from the current single-mask + output (based on output token 0) falls below a threshold, we instead select from + multi-mask outputs (based on output token 1~3) the mask with the highest predicted + IoU score. This is intended to ensure a valid mask for both clicking and tracking. + """ + # first, flatten the batch and the multiplex dimensions + B, M = all_mask_logits.shape[:2] + all_mask_logits = all_mask_logits.flatten(0, 1) + all_iou_scores = all_iou_scores.flatten(0, 1) + + # The best mask from multimask output tokens (1~3) + multimask_logits = all_mask_logits[:, 1:, :, :] + multimask_iou_scores = all_iou_scores[:, 1:] + best_scores_inds = torch.argmax(multimask_iou_scores, dim=-1) + batch_inds = torch.arange( + multimask_iou_scores.size(0), device=all_iou_scores.device + ) + best_multimask_logits = multimask_logits[batch_inds, best_scores_inds] + best_multimask_logits = best_multimask_logits.unsqueeze(1) + best_multimask_iou_scores = multimask_iou_scores[batch_inds, best_scores_inds] + best_multimask_iou_scores = best_multimask_iou_scores.unsqueeze(1) + + # The mask from singlemask output token 0 and its stability score + singlemask_logits = all_mask_logits[:, 0:1, :, :] + singlemask_iou_scores = all_iou_scores[:, 0:1] + stability_scores = self._get_stability_scores(singlemask_logits) + is_stable = stability_scores >= self.dynamic_multimask_stability_thresh + + # Dynamically fall back to best multimask output upon low stability scores. + mask_logits_out = torch.where( + is_stable[..., None, None].expand_as(singlemask_logits), + singlemask_logits, + best_multimask_logits, + ) + iou_scores_out = torch.where( + is_stable.expand_as(singlemask_iou_scores), + singlemask_iou_scores, + best_multimask_iou_scores, + ) + + # restore the batch and multiplex dimensions + mask_logits_out = mask_logits_out.unflatten(0, (B, M)) + iou_scores_out = iou_scores_out.unflatten(0, (B, M)) + + return mask_logits_out, iou_scores_out + + +# Lightly adapted from +# https://github.com/facebookresearch/MaskFormer/blob/main/mask_former/modeling/transformer/transformer_predictor.py # noqa +class MLP(nn.Module): + def __init__( + self, + input_dim: int, + hidden_dim: int, + output_dim: int, + num_layers: int, + sigmoid_output: bool = False, + ) -> None: + super().__init__() + self.num_layers = num_layers + h = [hidden_dim] * (num_layers - 1) + self.layers = nn.ModuleList( + nn.Linear(n, k) for n, k in zip([input_dim] + h, h + [output_dim]) + ) + self.sigmoid_output = sigmoid_output + + def forward(self, x): + for i, layer in enumerate(self.layers): + x = F.relu(layer(x)) if i < self.num_layers - 1 else layer(x) + if self.sigmoid_output: + x = F.sigmoid(x) + return x diff --git a/sam3/model/multiplex_utils.py b/sam3/model/multiplex_utils.py new file mode 100644 index 0000000..44f102a --- /dev/null +++ b/sam3/model/multiplex_utils.py @@ -0,0 +1,538 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved + +# pyre-unsafe + +import logging +import math +from typing import Optional + +import torch +from torch import nn + +# Special values for object tracking +_PADDING_NUM = -1 # Marks empty slots in buckets +_REMOVED_NUM = -1116 # Marks objects that have been removed + + +logger = logging.getLogger(__name__) + + +class MultiplexState: + """ + MultiplexState records the state of multiplexing, for one or more buckets. + + At a high level, we deal with the conversion of tensors between the data space (batch_size, num_channels, ...) + and the multiplex space (num_buckets, multiplex_count, num_channels, ...). + + The multiplex state stores the assignments of each batch element to a slot in a bucket. + Each bucket has a fixed number of slots (multiplex_count), and not all slots need to be filled. + The batch size should equate to total_valid_entries, which is the sum of the number of valid entries in each bucket. + + There are two main operations that this class supports: + mux: convert tensors in the data space to the multiplex space. + The mental model is that we start from a tensor of zeros that has the shape of the output, + then we go through the valid entries and place them into the corresponding slots, indicated by the assignments. + + demux: convert tensors in the multiplex space to the data space. + This is the reverse operation of mux. Note that zeros were used in mux for the padding slots, + and that those slots are ignored in demux. + + There are also two utility functions for object mangement: + add_objects: add new objects to the state by filling in empty slots + remove_objects: remove objects from the state by marking them as removed (not the same as empty!) + """ + + def __init__( + self, + assignments: list[list[int]], + device: torch.device, + dtype: torch.dtype, + allowed_bucket_capacity: int, + *, + object_ids: Optional[list[int]] = None, + ): + """ + assignments: a list of lists of object indices + Each top-level list represents a bucket + Each inner list represents the object indices that are in the bucket + The object indices must ranges from 0 to num_valid_entries - 1, except for the following special values (all negatives): + _PADDING_NUM, which denotes padding entries + _REMOVED_NUM, which denotes an pre-existing object that got removed (currently not used during init) + If you wish to save the "true" object IDs, i.e., during inference, you can bookkeep them here + """ + self.device = device + self.dtype = dtype + + # Initialize bucket assignments and precompute matrices + self.allowed_bucket_capacity = allowed_bucket_capacity + self._initialize_assignments(assignments, object_ids=object_ids) + + def _initialize_assignments( + self, assignments: list[list[int]], *, object_ids: Optional[list[int]] = None + ): + self.assignments = assignments + self.num_buckets = len(self.assignments) + if self.num_buckets == 0: + logger.error("No buckets found in the state") + raise ValueError("No buckets found in the state") + + self.multiplex_count = len(self.assignments[0]) + assert all( + len(self.assignments[i]) == self.multiplex_count + for i in range(self.num_buckets) + ) + + # number of non-negative elements in the state + self.total_valid_entries = sum( + sum(1 for x in bucket if x >= 0) for bucket in self.assignments + ) + self.total_non_padding_entries = sum( + sum(1 for x in bucket if x != _PADDING_NUM) for bucket in self.assignments + ) + + # check the validity of the object IDs + self.object_ids = object_ids + if self.object_ids is not None: + assert ( + len(self.object_ids) == self.total_valid_entries + ), "object_ids should map 1:1 to the valid entries" + + # check the validity of the assignments + all_object_idxs = set() + for bucket in self.assignments: + valid_entries_in_bucket = sum(1 for x in bucket if x != _PADDING_NUM) + assert ( + valid_entries_in_bucket <= self.allowed_bucket_capacity + ), f"{valid_entries_in_bucket=} > {self.allowed_bucket_capacity=}" + for obj_idx in bucket: + if obj_idx >= 0: + assert ( + obj_idx < self.total_non_padding_entries + ), f"object ID {obj_idx} >= {self.total_non_padding_entries}" + assert obj_idx not in all_object_idxs, "object IDs must be unique" + all_object_idxs.add(obj_idx) + + # Precompute and cache the actual selection matrices + self._precompute_transition_matrices(self.device, self.dtype) + + @property + def available_slots(self) -> int: + # returns the number of available slots in the state + return ( + self.num_buckets * self.allowed_bucket_capacity + - self.total_non_padding_entries + ) + + def find_next_batch_of_available_indices( + self, + num_objects: int, + *, + allow_new_buckets: bool = False, + prefer_new_buckets: bool = False, + ) -> list[int]: + # produce a list of consecutive indices that are available in the state + # Note: prefer_new_buckets parameter is accepted for API compatibility but not used here + # as the actual bucket allocation logic is in add_objects() + assert num_objects > 0, f"{num_objects=} must be positive" + if not allow_new_buckets: + assert ( + self.available_slots >= num_objects + ), f"not enough available slots {self.available_slots} < {num_objects}" + + return list( + range( + self.total_valid_entries, + self.total_valid_entries + num_objects, + ) + ) + + def add_objects( + self, + object_indices: list[int], + *, + object_ids: Optional[list[int]] = None, + allow_new_buckets: bool = False, + prefer_new_buckets: bool = False, + ): + """ + Add new objects to the state by filling in empty slots and + creating new buckets if necessary. + + object_indices must be sorted and follow existing object indices. + If prefer_new_buckets is True, we skip filling existing slots and place + the objects into freshly created buckets (requires allow_new_buckets=True). + """ + if len(object_indices) == 0: + return + + # we will modify this in-place + object_indices = object_indices.copy() + assert (object_ids is None) == ( + self.object_ids is None + ), "object_ids must either be always given or always omitted" + + if object_ids is not None: + assert len(object_ids) == len( + object_indices + ), "object_ids must have the same length as object_indices" + object_ids = object_ids.copy() + + num_new_objects = len(object_indices) + assert object_indices == sorted(object_indices), "object_indices must be sorted" + object_indices.reverse() # reverse so we can pop from the end + if object_ids is not None: + object_ids.reverse() + + if prefer_new_buckets: + assert allow_new_buckets, "prefer_new_buckets requires allow_new_buckets" + + slots_filled = 0 + buckets_created = 0 + + def _pop_next(): + idx = object_indices.pop() + if object_ids is not None and self.object_ids is not None: + self.object_ids.append(object_ids.pop()) + return idx + + if not prefer_new_buckets: + # Fill empty slots in existing buckets first + for bucket in self.assignments: + for i in range(self.allowed_bucket_capacity): + if bucket[i] == _PADDING_NUM: + bucket[i] = _pop_next() + slots_filled += 1 + if len(object_indices) == 0: + break + if len(object_indices) == 0: + break + + if len(object_indices) > 0 and not allow_new_buckets: + raise ValueError( + f"Cannot place objects {list(reversed(object_indices))} without creating new buckets" + ) + + # Create new buckets for remaining objects (or all objects if prefer_new_buckets) + while len(object_indices) > 0: + new_bucket = [_PADDING_NUM] * self.multiplex_count + for i in range(self.allowed_bucket_capacity): + if len(object_indices) == 0: + break + new_bucket[i] = _pop_next() + self.assignments.append(new_bucket) + buckets_created += 1 + + # reinitialize all the settings + original_num_entries = self.total_valid_entries + self._initialize_assignments(self.assignments, object_ids=self.object_ids) + assert ( + self.total_valid_entries == original_num_entries + num_new_objects + ), f"{self.total_valid_entries=} != {original_num_entries=} + {num_new_objects=}" + + logger.info( + f"Filled {slots_filled} slots and created {buckets_created} new buckets" + ) + logger.info( + f"{self.num_buckets=}, {self.total_valid_entries=}, {self.total_non_padding_entries=}" + ) + + def remove_objects(self, object_indices: list[int], strict: bool = True): + """ + Remove objects from the state by marking them as removed. + Remove a bucket if all objects in the bucket are removed. + + Args: + object_indices: List of object indices to remove + strict: If True, will raise an error if any object indices are not found in the state + + Returns: + List of bucket indices that we are going to keep + """ + object_indices = object_indices.copy() + + # Mark objects as removed in assignments + for bucket_idx, bucket in enumerate(self.assignments): + for slot_idx, obj_id in enumerate(bucket): + if obj_id in object_indices: + self.assignments[bucket_idx][slot_idx] = _REMOVED_NUM + object_indices.remove(obj_id) + + if strict: + assert ( + len(object_indices) == 0 + ), f"Failed to remove objects: {object_indices}" + + # Check which buckets should be completely removed (all objects removed/paddings) + # and which buckets we will keep + buckets_to_remove = [] + buckets_to_keep = [] + for bucket_idx, bucket in enumerate(self.assignments): + # Check if all objects in this bucket are removed or are paddings + all_removed = all( + obj_id in [_PADDING_NUM, _REMOVED_NUM] for obj_id in bucket + ) + if all_removed: + buckets_to_remove.append(bucket_idx) + logger.info( + f"Bucket {bucket_idx} marked for removal - all objects removed/paddings" + ) + else: + buckets_to_keep.append(bucket_idx) + + # Remove buckets in reverse order to maintain correct indices + for bucket_idx in reversed(buckets_to_remove): + del self.assignments[bucket_idx] + + if len(buckets_to_keep) == 0: + logger.info(f"Removing all buckets: {buckets_to_remove}; state invalidated") + self.assignments = None + if self.object_ids is not None: + self.object_ids = [] + return buckets_to_keep + + # After removal, remap object IDs to be sequential + # Collect all unique positive object IDs and create a mapping to sequential IDs + all_positive_ids = set() + for bucket in self.assignments: + for obj_id in bucket: + if obj_id >= 0: + all_positive_ids.add(obj_id) + + # Create mapping from old IDs to new sequential IDs + sorted_ids = sorted(all_positive_ids) + id_mapping = {old_id: new_id for new_id, old_id in enumerate(sorted_ids)} + + # Apply the mapping to assignments to make IDs sequential + for bucket in self.assignments: + for i, obj_id in enumerate(bucket): + if obj_id >= 0: + bucket[i] = id_mapping[obj_id] + + # Update object_ids if they exist + if self.object_ids is not None: + # Create new object_ids array based on the remapped indices + # We need to preserve the original object_ids for the objects that weren't removed + new_object_ids = [None] * len(sorted_ids) + + # Map the original object_ids to their new positions + for old_idx, new_idx in id_mapping.items(): + new_object_ids[new_idx] = self.object_ids[old_idx] + + assert not any(obj_id is None for obj_id in new_object_ids) + self.object_ids = new_object_ids + + # Reinitialize the state to update all internal structures + self._initialize_assignments(self.assignments, object_ids=self.object_ids) + + logger.info(f"Removed these buckets: {buckets_to_remove}") + logger.info(f"Kept these buckets: {buckets_to_keep}") + logger.info( + f"Remaining buckets: {self.num_buckets}, total valid entries: {self.total_valid_entries}" + ) + + return buckets_to_keep + + def _precompute_transition_matrices(self, device: torch.device, dtype: torch.dtype): + """ + Precompute the transition matrices for maximum efficiency. + Note that these should be partial permutation matrices. + """ + # Create a transition matrix for muxing + self.mux_matrix = torch.zeros( + self.num_buckets * self.multiplex_count, + self.total_valid_entries, + device=device, + dtype=dtype, + ) + + # Create a transition matrix for demuxing + self.demux_matrix = torch.zeros( + self.total_valid_entries, + self.num_buckets * self.multiplex_count, + device=device, + dtype=dtype, + ) + + # Fill both matrices based on assignments + for i in range(self.num_buckets): + for j in range(self.multiplex_count): + bucket_idx = i * self.multiplex_count + j + object_idx = self.assignments[i][j] + if object_idx >= 0: + self.mux_matrix[bucket_idx, object_idx] = 1.0 + self.demux_matrix[object_idx, bucket_idx] = 1.0 + + def mux(self, x: torch.Tensor) -> torch.Tensor: + """ + Multiplexing operation + x: self.total_valid_entries * ... + + return num_buckets * multiplex_count * ... + with padding entries filled with 0 + """ + num_valid_entries = x.shape[0] + assert ( + num_valid_entries == self.total_valid_entries + ), f"{num_valid_entries=} != {self.total_valid_entries=}" + output_shape = ( + self.num_buckets, + self.multiplex_count, + ) + x.shape[1:] + + x_flat = x.reshape(num_valid_entries, -1) + + # Apply mux matrix: (num_buckets * multiplex_count, batch_size) @ (batch_size, features) + # Result: (num_buckets * multiplex_count, features) + result_flat = self.mux_matrix @ x_flat + + result = result_flat.view(output_shape) + return result + + def demux(self, x: torch.Tensor) -> torch.Tensor: + """ + Inverse operation of mux + x: num_buckets, multiplex_count * ... + Returns: total_valid_entries * ... + """ + num_buckets, multiplex_count = x.shape[:2] + assert num_buckets == self.num_buckets, f"{num_buckets=} != {self.num_buckets=}" + assert ( + multiplex_count == self.multiplex_count + ), f"{multiplex_count=} != {self.multiplex_count=}" + output_shape = (self.total_valid_entries,) + x.shape[2:] + + x_flat = x.reshape(num_buckets * multiplex_count, -1) + + # Apply demux matrix: (total_valid_entries, num_buckets*multiplex_count) @ (num_buckets*multiplex_count, features) + # Result: (total_valid_entries, features) + result_flat = self.demux_matrix @ x_flat + + result = result_flat.view(output_shape) + return result + + def get_valid_object_mask(self) -> torch.Tensor: + """ + Returns a (num_buckets, multiplex_count) tensor with 1 for valid entries and 0 for padding entries + """ + valid_mask = self.mux_matrix.sum(dim=1) > 0 + valid_mask = valid_mask.reshape(self.num_buckets, self.multiplex_count) + + return valid_mask + + def get_all_valid_object_idx(self) -> set[int]: + """ + Returns a set of all valid object idx in the state + Note that this returns the internal object idx representations, + not the arbitrary object IDs that are passed in during initialization + """ + all_valid_objects = { + obj_idx for bucket in self.assignments for obj_idx in bucket if obj_idx >= 0 + } + return all_valid_objects + + +class MultiplexController(nn.Module): + def __init__( + self, + multiplex_count: int, + full_shuffle: bool = False, + eval_multiplex_count: int = -1, + ): + super().__init__() + + self.multiplex_count = multiplex_count + self.full_shuffle = full_shuffle + if eval_multiplex_count < 0: + self.eval_multiplex_count = multiplex_count + else: + self.eval_multiplex_count = eval_multiplex_count + assert self.multiplex_count >= 1 + + @property + def allowed_bucket_capacity(self) -> int: + if self.training: + return self.multiplex_count + else: + return self.eval_multiplex_count + + def get_state( + self, + num_valid_entries: int, + device: torch.device, + dtype: torch.dtype, + random: bool = True, + *, + object_ids: Optional[ + list[int] + ] = None, # object_ids is an auxiliary field that we pass to the state unmodified + ) -> MultiplexState: + # returns a state that maps elements in the batch to buckets of size self.multiplex_count + + allowed_bucket_capacity = self.allowed_bucket_capacity + + # the size of the bucket during training + true_bucket_capacity = self.multiplex_count + + num_buckets = math.ceil(num_valid_entries / allowed_bucket_capacity) + # each bucket contains at most self.multiplex_count elements + # padding elements are marked with _PADDING_NUM (only the last bucket should contain _PADDING_NUM) + + if self.full_shuffle: + # Shuffle all IDs, including the paddings + ids = torch.cat( + [ + torch.arange(num_valid_entries, dtype=torch.long), + torch.tensor( + [_PADDING_NUM] + * (num_buckets * true_bucket_capacity - num_valid_entries), + dtype=torch.long, + ), + ], + dim=0, + ) + if random: + indices = torch.randperm(ids.shape[0], dtype=torch.long) + ids = ids[indices] + + # convert to a list of list + assignments = [] + for i in range(num_buckets): + assignments.append( + ids[ + i * true_bucket_capacity : (i + 1) * true_bucket_capacity + ].tolist() + ) + else: + # Only shuffle the the IDs within the first #batch_size slots, leave all paddings at the end + if random: + # randomly assign ids to buckets + ids = torch.randperm(num_valid_entries, dtype=torch.int64) + else: + ids = torch.arange(num_valid_entries) + # append with _PADDING_NUM to make a multiple of bucket_capacity + total_elements = num_buckets * allowed_bucket_capacity + if ids.shape[0] < total_elements: + ids = torch.cat( + [ + ids, + torch.tensor([_PADDING_NUM] * (total_elements - ids.shape[0])), + ] + ) + + # convert to a list of list + assignments = [] + for i in range(num_buckets): + assignments.append( + ids[ + i * allowed_bucket_capacity : (i + 1) * allowed_bucket_capacity + ].tolist() + + [_PADDING_NUM] * (true_bucket_capacity - allowed_bucket_capacity) + ) + + return MultiplexState( + assignments, + device, + dtype, + allowed_bucket_capacity=allowed_bucket_capacity, + object_ids=object_ids, + ) diff --git a/sam3/model/necks.py b/sam3/model/necks.py index c60f87f..6db174e 100644 --- a/sam3/model/necks.py +++ b/sam3/model/necks.py @@ -9,6 +9,7 @@ import torch import torch.nn as nn +from sam3.model.data_misc import NestedTensor class Sam3DualViTDetNeck(nn.Module): @@ -124,3 +125,145 @@ def forward( sam2_out.append(sam2_x_out) sam2_pos.append(sam2_pos_out) return sam3_out, sam3_pos, sam2_out, sam2_pos + + +class Sam3TriViTDetNeck(nn.Module): + def __init__( + self, + trunk: nn.Module, + position_encoding: nn.Module, + d_model: int, + neck_norm=None, + scale_factors=(4.0, 2.0, 1.0), + ): + """ + SimpleFPN neck with three heads (sam3, interactive, propagation). + """ + super().__init__() + self.trunk = trunk + self.position_encoding = position_encoding + self.convs = nn.ModuleList() + + self.scale_factors = scale_factors + use_bias = neck_norm is None + dim = self.trunk.channel_list[-1] + + for _, scale in enumerate(scale_factors): + current = nn.Sequential() + + if scale == 4.0: + current.add_module( + "dconv_2x2_0", + nn.ConvTranspose2d(dim, dim // 2, kernel_size=2, stride=2), + ) + current.add_module( + "gelu", + nn.GELU(), + ) + current.add_module( + "dconv_2x2_1", + nn.ConvTranspose2d(dim // 2, dim // 4, kernel_size=2, stride=2), + ) + out_dim = dim // 4 + elif scale == 2.0: + current.add_module( + "dconv_2x2", + nn.ConvTranspose2d(dim, dim // 2, kernel_size=2, stride=2), + ) + out_dim = dim // 2 + elif scale == 1.0: + out_dim = dim + elif scale == 0.5: + current.add_module( + "maxpool_2x2", + nn.MaxPool2d(kernel_size=2, stride=2), + ) + out_dim = dim + else: + raise NotImplementedError(f"scale_factor={scale} is not supported yet.") + + current.add_module( + "conv_1x1", + nn.Conv2d( + in_channels=out_dim, + out_channels=d_model, + kernel_size=1, + bias=use_bias, + ), + ) + current.add_module( + "conv_3x3", + nn.Conv2d( + in_channels=d_model, + out_channels=d_model, + kernel_size=3, + padding=1, + bias=use_bias, + ), + ) + self.convs.append(current) + + # Assumes the new necks are just clones of the original neck + self.interactive_convs = deepcopy(self.convs) + self.propagation_convs = deepcopy(self.convs) + + def forward( + self, + tensor_list, + *, + need_sam3_out: bool = True, + need_interactive_out: bool = True, + need_propagation_out: bool = True, + ): + xs = self.trunk(tensor_list) + sam3_out = [] + interactive_out = [] + propagation_out = [] + + sam3_pos = [] + interactive_pos = [] + propagation_pos = [] + x = xs[-1] # simpleFPN + # OSS trunk returns plain tensors; onevision trunk returns NestedTensors. + # Use getattr to handle both in a torch.compile-friendly way. + x_data = getattr(x, "tensors", x) + x_mask = getattr(x, "mask", None) + for _, (conv, interactive_conv, propagation_conv) in enumerate( + zip(self.convs, self.interactive_convs, self.propagation_convs) + ): + if need_sam3_out: + sam3_conv_out = conv(x_data) + sam3_x_out = NestedTensor(sam3_conv_out, x_mask) + sam3_out.append(sam3_x_out) + sam3_pos.append( + self.position_encoding(sam3_conv_out).to(sam3_conv_out.dtype) + ) + + if need_interactive_out: + interactive_conv_out_t = interactive_conv(x_data) + interactive_conv_out = NestedTensor(interactive_conv_out_t, x_mask) + interactive_out.append(interactive_conv_out) + interactive_pos.append( + self.position_encoding(interactive_conv_out_t).to( + interactive_conv_out_t.dtype + ) + ) + + if need_propagation_out: + propagation_conv_out = propagation_conv(x_data) + propagation_x_out = NestedTensor(propagation_conv_out, x_mask) + propagation_out.append(propagation_x_out) + propagation_pos.append( + self.position_encoding(propagation_conv_out).to( + propagation_conv_out.dtype + ) + ) + + return ( + sam3_out, + sam3_pos, + interactive_out, + interactive_pos, + propagation_out, + propagation_pos, + ) diff --git a/sam3/model/position_encoding.py b/sam3/model/position_encoding.py index a6a1266..f38c62b 100644 --- a/sam3/model/position_encoding.py +++ b/sam3/model/position_encoding.py @@ -38,11 +38,17 @@ def __init__( # Precompute positional encodings under `precompute_resolution` to fill the cache # and avoid symbolic shape tracing errors in torch.compile in PyTorch 2.4 nightly. if precompute_resolution is not None: - # We precompute pos enc for stride 4, 8, 16 and 32 to fill `self.cache`. + # We precompute pos enc for all strides used by both DualViTDetNeck and + # TriViTDetNeck (scale_factors 4.0, 2.0, 1.0, 0.5 applied to backbone + # output at stride 14 from 1008px input → 72x72). precompute_sizes = [ + (int(precompute_resolution // 3.5), int(precompute_resolution // 3.5)), (precompute_resolution // 4, precompute_resolution // 4), + (int(precompute_resolution // 7), int(precompute_resolution // 7)), (precompute_resolution // 8, precompute_resolution // 8), + (int(precompute_resolution // 14), int(precompute_resolution // 14)), (precompute_resolution // 16, precompute_resolution // 16), + (int(precompute_resolution // 28), int(precompute_resolution // 28)), (precompute_resolution // 32, precompute_resolution // 32), ] for size in precompute_sizes: diff --git a/sam3/model/sam1_task_predictor.py b/sam3/model/sam1_task_predictor.py index 5cf0fde..edcbf87 100644 --- a/sam3/model/sam1_task_predictor.py +++ b/sam3/model/sam1_task_predictor.py @@ -94,9 +94,9 @@ def set_image( input_image = self._transforms(image) input_image = input_image[None, ...].to(self.device) - assert len(input_image.shape) == 4 and input_image.shape[1] == 3, ( - f"input_image must be of size 1x3xHxW, got {input_image.shape}" - ) + assert ( + len(input_image.shape) == 4 and input_image.shape[1] == 3 + ), f"input_image must be of size 1x3xHxW, got {input_image.shape}" logging.info("Computing image embeddings for the provided image...") backbone_out = self.model.forward_image(input_image) ( @@ -133,17 +133,17 @@ def set_image_batch( assert isinstance(image_list, list) self._orig_hw = [] for image in image_list: - assert isinstance(image, np.ndarray), ( - "Images are expected to be an np.ndarray in RGB format, and of shape HWC" - ) + assert isinstance( + image, np.ndarray + ), "Images are expected to be an np.ndarray in RGB format, and of shape HWC" self._orig_hw.append(image.shape[:2]) # Transform the image to the form expected by the model img_batch = self._transforms.forward_batch(image_list) img_batch = img_batch.to(self.device) batch_size = img_batch.shape[0] - assert len(img_batch.shape) == 4 and img_batch.shape[1] == 3, ( - f"img_batch must be of size Bx3xHxW, got {img_batch.shape}" - ) + assert ( + len(img_batch.shape) == 4 and img_batch.shape[1] == 3 + ), f"img_batch must be of size Bx3xHxW, got {img_batch.shape}" logging.info("Computing image embeddings for the provided images...") backbone_out = self.model.forward_image(img_batch) ( @@ -299,9 +299,9 @@ def _prep_prompts( ): unnorm_coords, labels, unnorm_box, mask_input = None, None, None, None if point_coords is not None: - assert point_labels is not None, ( - "point_labels must be supplied if point_coords is supplied." - ) + assert ( + point_labels is not None + ), "point_labels must be supplied if point_coords is supplied." point_coords = torch.as_tensor( point_coords, dtype=torch.float, device=self.device ) @@ -438,9 +438,9 @@ def get_image_embedding(self) -> torch.Tensor: raise RuntimeError( "An image must be set with .set_image(...) to generate an embedding." ) - assert self._features is not None, ( - "Features must exist if an image has been set." - ) + assert ( + self._features is not None + ), "Features must exist if an image has been set." return self._features["image_embed"] @property diff --git a/sam3/model/sam3_base_predictor.py b/sam3/model/sam3_base_predictor.py new file mode 100644 index 0000000..a02d213 --- /dev/null +++ b/sam3/model/sam3_base_predictor.py @@ -0,0 +1,322 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved + +# pyre-unsafe + +""" +Base predictor class shared by SAM3 and SAM3.1 (multiplex) video predictors. + +Provides the common handle_request/handle_stream_request API and session management. +Subclasses only need to override methods where their behavior differs. +""" + +import gc +import time +import uuid +from typing import Dict, List, Optional + +import torch +from sam3.logger import get_logger + +logger = get_logger(__name__) + + +class Sam3BasePredictor: + """ + Base class for SAM3 video predictors. Provides: + - Session management (start, reset, close) + - Request dispatch (handle_request / handle_stream_request) + - Common add_prompt / propagate_in_video / remove_object / reset_session / close_session + + Subclasses must set `self.model` and `self._all_inference_states` before use. + """ + + def __init__(self): + # Subclasses must populate these + self.model = None + self._all_inference_states: Dict[str, dict] = {} + + # ── Request dispatch ────────────────────────────────────────────── + + @torch.inference_mode() + def handle_request(self, request): + """Dispatch a request based on its type.""" + request_type = request["type"] + if request_type == "start_session": + return self.start_session( + resource_path=request["resource_path"], + session_id=request.get("session_id", None), + offload_video_to_cpu=request.get("offload_video_to_cpu", False), + ) + elif request_type == "add_prompt": + return self.add_prompt( + session_id=request["session_id"], + frame_idx=request["frame_index"], + text=request.get("text", None), + points=request.get("points", None), + point_labels=request.get("point_labels", None), + clear_old_points=request.get("clear_old_points", True), + bounding_boxes=request.get("bounding_boxes", None), + bounding_box_labels=request.get("bounding_box_labels", None), + clear_old_boxes=request.get("clear_old_boxes", True), + output_prob_thresh=request.get( + "output_prob_thresh", + getattr(self, "default_output_prob_thresh", 0.5), + ), + obj_id=request.get("obj_id", None), + ) + elif request_type == "remove_object": + return self.remove_object( + session_id=request["session_id"], + frame_idx=request.get("frame_index", 0), + obj_id=request["obj_id"], + ) + elif request_type == "reset_session": + return self.reset_session(session_id=request["session_id"]) + elif request_type == "cancel_propagation": + return self.cancel_propagation(session_id=request["session_id"]) + elif request_type == "close_session": + return self.close_session( + session_id=request["session_id"], + run_gc_collect=request.get("run_gc_collect", True), + ) + else: + raise RuntimeError(f"invalid request type: {request_type}") + + @torch.inference_mode() + def handle_stream_request(self, request): + """Dispatch a stream request based on its type.""" + request_type = request["type"] + if request_type == "propagate_in_video": + yield from self.propagate_in_video( + session_id=request["session_id"], + propagation_direction=request.get("propagation_direction", "both"), + start_frame_idx=request.get("start_frame_index", None), + max_frame_num_to_track=request.get("max_frame_num_to_track", None), + output_prob_thresh=request.get( + "output_prob_thresh", + getattr(self, "default_output_prob_thresh", 0.5), + ), + ) + else: + raise RuntimeError(f"invalid request type: {request_type}") + + # ── Session management ──────────────────────────────────────────── + + def start_session( + self, + resource_path, + session_id=None, + offload_video_to_cpu=False, + ): + """Start a new inference session on a video directory or path.""" + init_kwargs = dict( + resource_path=resource_path, + offload_video_to_cpu=offload_video_to_cpu, + ) + if hasattr(self, "async_loading_frames"): + init_kwargs["async_loading_frames"] = self.async_loading_frames + if hasattr(self, "video_loader_type"): + init_kwargs["video_loader_type"] = self.video_loader_type + inference_state = self.model.init_state(**init_kwargs) + + if not session_id: + session_id = str(uuid.uuid4()) + self._all_inference_states[session_id] = { + "state": inference_state, + "session_id": session_id, + "start_time": time.time(), + "last_use_time": time.time(), + } + logger.info(f"started new session {session_id}") + return {"session_id": session_id} + + def add_prompt( + self, + session_id: str, + frame_idx: int, + text: Optional[str] = None, + points=None, + point_labels=None, + clear_old_points: bool = True, + bounding_boxes=None, + bounding_box_labels=None, + clear_old_boxes: bool = True, + output_prob_thresh: float = 0.5, + obj_id: Optional[int] = None, + ): + """Add text, box and/or point prompt on a specific video frame.""" + session = self._get_session(session_id) + inference_state = session["state"] + self._extend_expiration_time(session) + + # Convert lists to tensors if needed + if points is not None and not isinstance(points, torch.Tensor): + points = torch.tensor(points, dtype=torch.float32) + if point_labels is not None and not isinstance(point_labels, torch.Tensor): + point_labels = torch.tensor(point_labels, dtype=torch.int32) + if bounding_boxes is not None and not isinstance(bounding_boxes, torch.Tensor): + bounding_boxes = torch.tensor(bounding_boxes, dtype=torch.float32) + if bounding_box_labels is not None and not isinstance( + bounding_box_labels, torch.Tensor + ): + bounding_box_labels = torch.tensor(bounding_box_labels, dtype=torch.int32) + + kwargs = dict( + inference_state=inference_state, + frame_idx=frame_idx, + text_str=text, + points=points, + point_labels=point_labels, + clear_old_points=clear_old_points, + boxes_xywh=bounding_boxes, + box_labels=bounding_box_labels, + clear_old_boxes=clear_old_boxes, + output_prob_thresh=output_prob_thresh, + ) + if obj_id is not None: + kwargs["obj_id"] = obj_id + + # Filter kwargs to only pass what the model accepts + # (SAM3 has a simpler add_prompt than SAM3.1) + import inspect + + sig = inspect.signature(self.model.add_prompt) + valid_params = set(sig.parameters.keys()) + filtered_kwargs = {k: v for k, v in kwargs.items() if k in valid_params} + + frame_idx, outputs = self.model.add_prompt(**filtered_kwargs) + return {"frame_index": frame_idx, "outputs": outputs} + + def remove_object( + self, + session_id: str, + frame_idx: int = 0, + obj_id: int = 0, + is_user_action: bool = True, + ): + """Remove an object from tracking.""" + session = self._get_session(session_id) + inference_state = session["state"] + self._extend_expiration_time(session) + + result = self.model.remove_object( + inference_state, obj_id, frame_idx=frame_idx, is_user_action=is_user_action + ) + # Handle both return conventions + if result is None or (isinstance(result, tuple) and result[1] is None): + import numpy as np + + out_obj_ids = torch.zeros(0, dtype=torch.int64) + out_binary_masks = torch.zeros( + 0, + inference_state["orig_height"], + inference_state["orig_width"], + dtype=torch.bool, + ) + out_boxes_xywh = torch.zeros(0, 4, dtype=torch.float32) + outputs = { + "out_obj_ids": out_obj_ids.cpu().numpy(), + "out_boxes_xywh": out_boxes_xywh.cpu().numpy(), + "out_binary_masks": out_binary_masks.cpu().numpy(), + } + elif isinstance(result, tuple): + _, outputs = result + else: + outputs = result + return {"frame_index": frame_idx, "outputs": outputs} + + def cancel_propagation(self, session_id): + """Cancel any ongoing propagation. No-op if not supported by the model.""" + session = self._get_session(session_id) + inference_state = session["state"] + self._extend_expiration_time(session) + if hasattr(self.model, "cancel_propagation"): + self.model.cancel_propagation(inference_state) + return {"is_success": True} + + def propagate_in_video( + self, + session_id, + propagation_direction="both", + start_frame_idx=None, + max_frame_num_to_track=None, + output_prob_thresh=0.5, + **kwargs, + ): + """Propagate the added prompts to get results on all video frames.""" + try: + session = self._get_session(session_id) + inference_state = session["state"] + self._extend_expiration_time(session) + if propagation_direction not in ["both", "forward", "backward"]: + raise ValueError( + f"invalid propagation direction: {propagation_direction}" + ) + + propagate_kwargs = dict( + inference_state=inference_state, + start_frame_idx=start_frame_idx, + max_frame_num_to_track=max_frame_num_to_track, + ) + # Only pass output_prob_thresh / extra kwargs if the model supports them + import inspect + + sig = inspect.signature(self.model.propagate_in_video) + if "output_prob_thresh" in sig.parameters: + propagate_kwargs["output_prob_thresh"] = output_prob_thresh + for k, v in kwargs.items(): + if k in sig.parameters: + propagate_kwargs[k] = v + + # Forward propagation + if propagation_direction in ["both", "forward"]: + for frame_idx, outputs in self.model.propagate_in_video( + **propagate_kwargs, + reverse=False, + ): + yield {"frame_index": frame_idx, "outputs": outputs} + # Backward propagation + if propagation_direction in ["both", "backward"]: + for frame_idx, outputs in self.model.propagate_in_video( + **propagate_kwargs, + reverse=True, + ): + yield {"frame_index": frame_idx, "outputs": outputs} + finally: + logger.info(f"propagation ended in session {session_id}") + + def reset_session(self, session_id): + """Reset the session to its initial state.""" + session = self._get_session(session_id) + inference_state = session["state"] + self._extend_expiration_time(session) + self.model.reset_state(inference_state) + return {"is_success": True} + + def close_session(self, session_id, run_gc_collect=True): + """Close a session. Idempotent.""" + session = self._all_inference_states.pop(session_id, None) + if session is None: + logger.warning(f"cannot close session {session_id} as it does not exist") + else: + del session + if run_gc_collect: + gc.collect() + logger.info(f"removed session {session_id}") + return {"is_success": True} + + def _get_session(self, session_id): + session = self._all_inference_states.get(session_id, None) + if session is None: + raise RuntimeError( + f"Cannot find session {session_id}; it might have expired" + ) + return session + + def _extend_expiration_time(self, session): + """Update last-use time for session expiration tracking.""" + session["last_use_time"] = time.time() + + def shutdown(self): + """Shutdown the predictor and clear all sessions.""" + self._all_inference_states.clear() diff --git a/sam3/model/sam3_image.py b/sam3/model/sam3_image.py index 679300d..9371432 100644 --- a/sam3/model/sam3_image.py +++ b/sam3/model/sam3_image.py @@ -16,6 +16,7 @@ from .act_ckpt_utils import activation_ckpt_wrapper from .box_ops import box_cxcywh_to_xyxy +from .data_misc import FindStage from .geometry_encoders import Prompt from .model_misc import inverse_sigmoid @@ -442,6 +443,7 @@ def forward_grounding( find_input, find_target, geometric_prompt: Prompt, + **kwargs, ): with torch.profiler.record_function("SAM3Image._encode_prompt"): prompt, prompt_mask, backbone_out = self._encode_prompt( @@ -474,10 +476,14 @@ def forward_grounding( # Run segmentation heads with torch.profiler.record_function("SAM3Image._run_segmentation_heads"): + # Apply id_mapping to img_ids if backbone features were recomputed + seg_img_ids = find_input.img_ids + if "id_mapping" in backbone_out and backbone_out["id_mapping"] is not None: + seg_img_ids = backbone_out["id_mapping"][seg_img_ids] self._run_segmentation_heads( out=out, backbone_out=backbone_out, - img_ids=find_input.img_ids, + img_ids=seg_img_ids, vis_feat_sizes=encoder_out["vis_feat_sizes"], encoder_hidden_states=out["encoder_hidden_states"], prompt=prompt, @@ -516,6 +522,28 @@ def _postprocess_out(self, out: Dict, multimask_output: bool = False): return out + def _get_geo_prompt_from_find_input(self, find_input: FindStage): + """Construct an initial geometric prompt from the find input.""" + point_embeddings, point_mask, point_labels = None, None, None + if find_input.input_points_before_embed is not None: + # Point embeddings are batch first, switch to seq first + point_embeddings = find_input.input_points_before_embed.transpose(0, 1) + + # they are stored as (x,y,label), so we unpack + point_labels = point_embeddings[..., -1] + point_embeddings = point_embeddings[..., :-1] + point_mask = find_input.input_points_mask + + geometric_prompt = Prompt( + box_embeddings=find_input.input_boxes_before_embed, + box_mask=find_input.input_boxes_mask, + box_labels=find_input.input_boxes_label, + point_embeddings=point_embeddings, + point_mask=point_mask, + point_labels=point_labels, + ) + return geometric_prompt + def _get_dummy_prompt(self, num_prompts=1): device = self.device geometric_prompt = Prompt( @@ -656,9 +684,9 @@ def predict_inst_batch( inference_state["original_heights"], inference_state["original_widths"], ) - assert batch_size == len(orig_heights) == len(orig_widths), ( - f"Batch size mismatch in predict_inst_batch. Got {batch_size}, {len(orig_heights)}, {len(orig_widths)}" - ) + assert ( + batch_size == len(orig_heights) == len(orig_widths) + ), f"Batch size mismatch in predict_inst_batch. Got {batch_size}, {len(orig_heights)}, {len(orig_widths)}" feats = [ feat.permute(1, 2, 0).view(batch_size, -1, *feat_size) for feat, feat_size in zip( diff --git a/sam3/model/sam3_image_processor.py b/sam3/model/sam3_image_processor.py index 83bbb0d..d3b6206 100644 --- a/sam3/model/sam3_image_processor.py +++ b/sam3/model/sam3_image_processor.py @@ -81,9 +81,9 @@ def set_image_batch(self, images: List[np.ndarray], state=None): if not isinstance(images, list): raise ValueError("Images must be a list of PIL images or tensors") assert len(images) > 0, "Images list must not be empty" - assert isinstance(images[0], PIL.Image.Image), ( - "Images must be a list of PIL images" - ) + assert isinstance( + images[0], PIL.Image.Image + ), "Images must be a list of PIL images" state["original_heights"] = [image.height for image in images] state["original_widths"] = [image.width for image in images] diff --git a/sam3/model/sam3_multiplex_base.py b/sam3/model/sam3_multiplex_base.py new file mode 100644 index 0000000..3e3e639 --- /dev/null +++ b/sam3/model/sam3_multiplex_base.py @@ -0,0 +1,2856 @@ +import datetime +import logging +import math +import os +import sys +from collections import defaultdict +from copy import deepcopy +from typing import Any, Dict, List, Optional, Set, Tuple + +import numpy as np +import torch +import torch.distributed as dist +import torch.nn.functional as F +from sam3.logger import get_logger +from sam3.model.box_ops import fast_diag_box_iou +from sam3.model.data_misc import BatchedDatapoint, NestedTensor +from sam3.model.sam3_multiplex_detector import Sam3MultiplexDetector +from sam3.model.sam3_tracker_utils import fill_holes_in_mask_scores, mask_to_box +from sam3.model.sam3_video_base import ( + _associate_det_trk_compilable, + LazyAssociateDetTrkResult, + MaskletConfirmationStatus, + realize_adt_result, + RealizedAssociateDetTrkresult, + Sam3VideoBase, +) +from sam3.perflib.masks_ops import mask_iou +from sam3.train.masks_ops import rle_encode +from torch import nn, Tensor + +# a short 3-min timeout to quickly detect any synchronization failures +SAM3_COLLECTIVE_OP_TIMEOUT_SEC = int(os.getenv("SAM3_COLLECTIVE_OP_TIMEOUT_SEC", "180")) + +logger = get_logger(__name__) + +if torch.cuda.get_device_properties(0).major >= 8: + # turn on tfloat32 for Ampere GPUs (https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices) + torch.backends.cuda.matmul.allow_tf32 = True + torch.backends.cudnn.allow_tf32 = True + + +class Sam3MultiplexTrackerPredictor(nn.Module): + def __init__( + self, + config_file, + checkpoint_file=None, + hydra_overrides=None, + per_obj_inference=False, + fill_hole_area=0, + use_fa3=False, + use_rope_real=False, + keep_first_cond_frame=False, + is_multiplex=False, + is_multiplex_dynamic=False, + use_memory_selection=False, + ): + """ + Initialize the SAM2 predictor with the given configuration and checkpoint. + Args: + config_file (str): Path to the configuration file. + checkpoint_file (str, optional): Path to the checkpoint file. If None, the model will be initialized without loading weights. + hydra_overrides (list, optional): List of Hydra overrides to apply to the configuration. + per_obj_inference (bool): If True, the model will perform per-object inference instead of bucketized batching. + """ + + super().__init__() + ####################################### + # Load model from config and checkpoint + ####################################### + + from hydra import compose, initialize_config_module + from hydra.core.global_hydra import GlobalHydra + from hydra.utils import instantiate + + # Ensure proper Hydra initialization + if not GlobalHydra().is_initialized(): + logger.info("Sam3MultiplexTrackerPredictor: GlobalHydra not initialized") + GlobalHydra.instance().clear() + initialize_config_module("sam3.config", version_base="1.2") + + if hydra_overrides is None: + hydra_overrides = [] + self.is_multiplex = is_multiplex + self.is_multiplex_dynamic = is_multiplex_dynamic + self.per_obj_inference = per_obj_inference + + if self.is_multiplex: + inference_model_class = "sam3.model.video_tracking_multiplex_demo.Sam3VideoTrackingMultiplexDemo" + else: + inference_model_class = ( + "sam3.model.video_tracking_with_prompt_demo_per_obj_inference.Sam3VideoTrackingWithPromptDemoPerObjInference" + if per_obj_inference + else "sam3.model.video_tracking_with_prompt_demo.Sam3VideoTrackingWithPromptDemo" + ) + hydra_overrides = list(hydra_overrides) + hydra_overrides.extend( + [ + "launcher.experiment_log_dir=''", + f"++trainer.model._target_={inference_model_class}", + # Shared backbone cfg + "++trainer.model.image_size=1008", + "++trainer.model.backbone_stride=14", + "++trainer.model.maskmem_backbone.mask_downsampler.interpol_size=[1152,1152]", + "++trainer.model.backbone.forward_in_chunk_for_eval=false", + # always start tracking from the frame where we receive the first annotation + # (clicks or mask) and ignore the `start_frame_idx` passed to `propagate_in_video` + "++trainer.model.always_start_from_first_ann_frame=false", + # apply non-overlapping constraints on the object masks in the + # memory encoder to avoid/alleviate superposing mask predictions + "++trainer.model.non_overlap_masks_for_mem_enc=false", + # Do not apply non-overlapping constraints on the output + "++trainer.model.non_overlap_masks_for_output=false", + # attend to at most 4 temporally closest conditioning frames in the encoder for + # better temporal locality and a better handling to a large number of annotated frames + "++trainer.model.max_cond_frames_in_attn=4", + f"++trainer.model.keep_first_cond_frame={keep_first_cond_frame}", + # turn off all offloading options in the demo (we handle them separately in the demo class) + "++trainer.model.offload_output_to_cpu_for_eval=false", + "++trainer.model.trim_past_non_cond_mem_for_eval=false", + # torch.compile on the image backbone (w/ `dynamic=false` and `fullgraph=true` to capture a full graph) + # "++trainer.model.backbone.compile_mode=max-autotune", + # "++trainer.model.backbone.compile_extra_args.fullgraph=true", + # "++trainer.model.backbone.compile_extra_args.dynamic=false", + "++trainer.model.backbone.visual.trunk.weights_path=null", + # Postprocessing/demo options + # dynamically fall back to multi-mask if the single mask is not stable + "++trainer.model.sam_mask_decoder_extra_args.dynamic_multimask_via_stability=true", + "++trainer.model.sam_mask_decoder_extra_args.dynamic_multimask_stability_delta=0.05", + "++trainer.model.sam_mask_decoder_extra_args.dynamic_multimask_stability_thresh=0.98", + # the sigmoid mask logits on interacted frames with clicks in the memory encoder so that the encoded masks are exactly as what users see from clicking + "++trainer.model.binarize_mask_from_pts_for_mem_enc=true", + # only attend to object pointers in the past (before the current frame) in the encoder during evaluation + "++trainer.model.only_obj_ptrs_in_the_past_for_eval=true", + # clear non-conditioning memory of the surrounding frames (which may contain outdated information) after adding correction clicks + "++trainer.model.clear_non_cond_mem_around_input=true", + "++trainer.model.transformer.encoder.layer.self_attention.feat_sizes=[72,72]", + "++trainer.model.transformer.encoder.layer.cross_attention.feat_sizes=[72,72]", + # fill small holes in the final masks up to `fill_hole_area` (after resizing them to the original video resolution) + f"++trainer.model.fill_hole_area={fill_hole_area}", + f"++trainer.model.transformer.encoder.layer.self_attention.use_fa3={use_fa3}", + f"++trainer.model.transformer.encoder.layer.cross_attention.use_fa3={use_fa3}", + f"++trainer.model.transformer.encoder.layer.self_attention.use_rope_real={use_rope_real}", + f"++trainer.model.transformer.encoder.layer.cross_attention.use_rope_real={use_rope_real}", + ] + ) + + if self.is_multiplex or self.is_multiplex_dynamic: + hydra_overrides.extend( + [ + f"++trainer.model.transformer.encoder.layer.self_attention_rope.use_fa3={use_fa3}", + f"++trainer.model.transformer.encoder.layer.cross_attention_rope.use_fa3={use_fa3}", + f"++trainer.model.transformer.encoder.layer.self_attention_rope.use_rope_real={use_rope_real}", + f"++trainer.model.transformer.encoder.layer.cross_attention_rope.use_rope_real={use_rope_real}", + ] + ) + + hydra_overrides.extend( + [f"++trainer.model.use_memory_selection={use_memory_selection}"] + ) + + cfg = compose(config_name=config_file, overrides=hydra_overrides) + model = instantiate(cfg.trainer.model, _recursive_=True) + del model.backbone # Remove backbone since it is shared with the sam3 model + if checkpoint_file is not None: + ckpt = torch.load(checkpoint_file, map_location="cpu") + model.load_state_dict(ckpt["model"], strict=False) + self.model = model + self.per_obj_inference = per_obj_inference + self.fill_hole_area = fill_hole_area + # use bfloat16 inference for Flash Attention kernel + self.bf16_context = torch.autocast(device_type="cuda", dtype=torch.bfloat16) + self.bf16_context.__enter__() # keep using for the entire model process + + def __getattr__(self, name): + # Expose all attributes of the underlying model + model = super().__getattr__("model") + if name == "model": + return model + return getattr(model, name) + + def forward(self, *args, **kwargs): + raise NotImplementedError( + "Use the sam2 predictor APIs instead. Check VideoTrackingWithPromptDemo class for details." + ) + + def add_output_per_object(self, *args, **kwargs): + if self.per_obj_inference: + # nothing needs to be done as each object is already stored separately + return + + # for batched inference state, we also need to add per-object + # memory slides to support instance interactivity + self._add_output_per_object(*args, **kwargs) + + +class Sam3MultiplexBase(Sam3VideoBase): + def __init__( + self, + tracker, + detector, + ckpt_path=None, + sam3_ckpt_path=None, + # prob threshold for detection outputs -- only keep detections above this threshold + # enters NMS and det-to-track matching + score_threshold_detection=0.5, + # Detection threshold when running on image-only inputs + image_only_det_thresh=0.5, + # IoU threshold for detection NMS + det_nms_thresh=0.0, + # If `det_nms_use_iom` is True, use IoM instead of IoU for NMS + det_nms_use_iom=False, + # IoU threshold for det-to-track matching -- a detection is considered "matched" to a tracklet it + # overlaps with a tracklet above this threshold -- it is often a loose threshold like 0.1 + assoc_iou_thresh=0.5, + # IoU threshold for det-to-track matching, which is used to determine whether a masklet is "unmatched" + # by any detections -- it is often a stricter threshold like 0.5 + trk_assoc_iou_thresh=0.5, + # prob threshold for a detection to be added as a new object + new_det_thresh=0.5, + # hotstart parameters: we hold off the outputs for `hotstart_delay` frames and + # 1) remove those tracklets unmatched by any detections based on `hotstart_unmatch_thresh` + # 2) remove those tracklets overlapping with one another based on `hotstart_dup_thresh` + hotstart_delay=0, + hotstart_unmatch_thresh=3, + hotstart_dup_thresh=3, + # Whether to suppress masks only within hotstart. If False, we can suppress masks even if they start before hotstart period. + suppress_unmatched_only_within_hotstart=True, + init_trk_keep_alive=0, + max_trk_keep_alive=8, + min_trk_keep_alive=-4, + # Threshold for suppressing overlapping objects based on recent occlusion + suppress_overlapping_based_on_recent_occlusion_threshold=0.0, + allow_unoccluded_to_suppress: bool = False, + decrease_trk_keep_alive_for_empty_masklets=False, + o2o_matching_masklets_enable=False, # Enable hungarian matching to match existing masklets + suppress_det_close_to_boundary=False, + fill_hole_area=16, + sprinkle_removal_area=16, + # The maximum number of objects (masklets) to track across all GPUs (for no limit, set it to -1) + max_num_objects=128, # 128 objects (total across all GPUs) should be able to cover nearly all cases + max_num_kboxes=20, + recondition_every_nth_frame=-1, + use_iom_recondition=False, + iom_thresh_recondition=0.8, + iou_thresh_recondition=0.8, + is_multiplex=False, + # masket confirmation status (to suppress unconfirmed masklets) + masklet_confirmation_enable=False, + # a masklet is confirmed after being consecutively detected and matched for + # `masklet_confirmation_consecutive_det_thresh` + masklet_confirmation_consecutive_det_thresh=3, + # bbox heuristic parameters + reconstruction_bbox_iou_thresh=0.0, + reconstruction_bbox_det_score=0.5, + reapply_no_object_pointer: bool = False, # reapply the no object pointer for suppressed objects + running_in_prod=False, # Flag to specify if we are running in FBInfra for Insta Edit/Segments + use_batched_grounding=False, + batched_grounding_batch_size=1, + **kwargs, + ): + nn.Module.__init__(self) + assert isinstance(tracker, Sam3MultiplexTrackerPredictor) + self.tracker = tracker + assert isinstance(detector, Sam3MultiplexDetector) + self.detector = detector + if sam3_ckpt_path: + ckpt = torch.load(sam3_ckpt_path, map_location="cpu", weights_only=True) + self.detector.load_state_dict(ckpt["model"], strict=False) + elif ckpt_path: + self._load_checkpoint(ckpt_path, strict=False) + self.score_threshold_detection = score_threshold_detection + self.image_only_det_thresh = image_only_det_thresh + self.det_nms_thresh = det_nms_thresh + self.det_nms_use_iom = det_nms_use_iom + self.assoc_iou_thresh = assoc_iou_thresh + self.trk_assoc_iou_thresh = trk_assoc_iou_thresh + self.new_det_thresh = new_det_thresh + self.is_multiplex = is_multiplex + self.running_in_prod = running_in_prod + self.detector.running_in_prod = running_in_prod + + assert ( + self.is_multiplex == self.tracker.is_multiplex == self.detector.is_multiplex + ), f"is_multiplex must be the same for all models: {self.is_multiplex=}, {self.tracker.is_multiplex=}, {self.detector.is_multiplex=}" + + # hotstart parameters + if hotstart_delay > 0: + assert hotstart_unmatch_thresh <= hotstart_delay + assert hotstart_dup_thresh <= hotstart_delay + self.hotstart_delay = hotstart_delay + self.hotstart_unmatch_thresh = hotstart_unmatch_thresh + self.hotstart_dup_thresh = hotstart_dup_thresh + self.suppress_unmatched_only_within_hotstart = ( + suppress_unmatched_only_within_hotstart + ) + self.init_trk_keep_alive = init_trk_keep_alive + self.max_trk_keep_alive = max_trk_keep_alive + self.min_trk_keep_alive = min_trk_keep_alive + self.suppress_overlapping_based_on_recent_occlusion_threshold = ( + suppress_overlapping_based_on_recent_occlusion_threshold + ) + self.allow_unoccluded_to_suppress = allow_unoccluded_to_suppress + self.suppress_det_close_to_boundary = suppress_det_close_to_boundary + self.decrease_trk_keep_alive_for_empty_masklets = ( + decrease_trk_keep_alive_for_empty_masklets + ) + self.o2o_matching_masklets_enable = o2o_matching_masklets_enable + self.fill_hole_area = fill_hole_area + self.sprinkle_removal_area = sprinkle_removal_area + self.eval() + self.rank = int(os.getenv("RANK", "0")) + self.world_size = int(os.getenv("WORLD_SIZE", "1")) + self._dist_pg_cpu = None # CPU process group (lazy-initialized on first use) + + # Initialize profiling variables + self._profiler = None + self._frame_count = 0 + self._profile_save_dir = os.getenv("PROFILE_SAVE_DIR", "/tmp/profiling") + self._profiling_enabled = os.getenv("ENABLE_PROFILING", "0").lower() == "1" + + # the maximum object number + if max_num_objects > 0: + multiplex_divisor = ( + self.tracker.multiplex_controller.allowed_bucket_capacity + if self.is_multiplex + else 1 + ) + num_obj_for_compile = math.ceil( + max_num_objects / (self.world_size * multiplex_divisor) + ) + else: + max_num_objects = 10000 # no limit + num_obj_for_compile = 16 + logger.info( + f"`setting max_num_objects` to {max_num_objects} -- creating {num_obj_for_compile=} objects for torch.compile cache" + ) + self.max_num_objects = max_num_objects + self.num_obj_for_compile = num_obj_for_compile + self.max_num_kboxes = max_num_kboxes + self.recondition_every_nth_frame = recondition_every_nth_frame + self.use_iom_recondition = use_iom_recondition + self.iom_thresh_recondition = iom_thresh_recondition + self.iou_thresh_recondition = iou_thresh_recondition + self.masklet_confirmation_enable = masklet_confirmation_enable + self.masklet_confirmation_consecutive_det_thresh = ( + masklet_confirmation_consecutive_det_thresh + ) + self.reconstruction_bbox_iou_thresh = reconstruction_bbox_iou_thresh + self.reconstruction_bbox_det_score = reconstruction_bbox_det_score + self.reapply_no_object_pointer = reapply_no_object_pointer + + # Batched grounding configuration + self.use_batched_grounding = use_batched_grounding + self.batched_grounding_batch_size = ( + batched_grounding_batch_size # Batch size for batched grounding + ) + + if self.is_multiplex: + assert ( + not self.tracker.multiplex_controller.training + ), "This model class should only be used for eval." + self.bucket_capacity: int = ( + self.tracker.multiplex_controller.allowed_bucket_capacity + ) + + def all_gather_cpu(self, tensor_list, tensor): + if self._dist_pg_cpu is None: + self._init_dist_pg_cpu() + dist.broadcast(tensor_list, tensor, group=self._dist_pg_cpu) + + def all_gather_python_obj_cpu(self, object_list, python_obj): + if self._dist_pg_cpu is None: + self._init_dist_pg_cpu() + dist.all_gather_object(object_list, python_obj, group=self._dist_pg_cpu) + + def broadcast_cpu(self, x, src): + if self._dist_pg_cpu is None: + self._init_dist_pg_cpu() + dist.broadcast(x, src=src, group=self._dist_pg_cpu) + + def _start_profiling(self, frame_idx): + self._profiling_enabled = os.getenv("ENABLE_PROFILING", "0").lower() == "1" + self._profile_end_frame = int(os.getenv("PROFILE_END_FRAME", "-1")) + """Start profiling for _det_track_one_frame if conditions are met.""" + if not self._profiling_enabled: + return False + + if not getattr(self, "_warm_up_complete", False): + return False + + if self._profiler is not None: + return True + + # Start profiling + os.makedirs(self._profile_save_dir, exist_ok=True) + profile_path = os.path.join( + self._profile_save_dir, f"det_track_frame_rank_{self.rank}.json.gz" + ) + + self._profiler = torch.profiler.profile( + activities=[ + torch.profiler.ProfilerActivity.CPU, + torch.profiler.ProfilerActivity.CUDA, + ], + record_shapes=True, + experimental_config=torch.profiler._ExperimentalConfig( + profile_all_threads=True + ), + ) + self._profiler.start() + self._current_profile_path = profile_path + print(f"Started profiling frame on {frame_idx} on rank {self.rank}") + return True + + def _stop_profiling(self): + """Stop profiling and save trace.""" + if self._profiler is not None: + self._profiler.stop() + self._profiler.export_chrome_trace(self._current_profile_path) + print(f"Profiling trace saved to: {self._current_profile_path}") + print( + f"You can open this file in Perfetto (https://ui.perfetto.dev/) to visualize the trace" + ) + self._profiler = None + self._profiling_enabled = False + os.environ["ENABLE_PROFILING"] = "0" + + def _det_track_one_frame( + self, + frame_idx: int, + num_frames: int, + reverse: bool, + input_batch: BatchedDatapoint, + geometric_prompt: Any, + tracker_states_local: List[Any], + tracker_metadata_prev: Dict[str, Any], + feature_cache: Dict, + orig_vid_height: int, + orig_vid_width: int, + is_image_only: bool = False, + ): + profiling_enabled = self._start_profiling(frame_idx) + + try: + return self._det_track_one_frame_impl( + frame_idx=frame_idx, + num_frames=num_frames, + reverse=reverse, + input_batch=input_batch, + geometric_prompt=geometric_prompt, + tracker_states_local=tracker_states_local, + tracker_metadata_prev=tracker_metadata_prev, + feature_cache=feature_cache, + orig_vid_height=orig_vid_height, + orig_vid_width=orig_vid_width, + is_image_only=is_image_only, + ) + finally: + if profiling_enabled: + if sys.exc_info()[0] is not None: + # If there is an exception, stop profiling + self._stop_profiling() + else: + if ( + (not reverse and frame_idx == num_frames - 1) + or (reverse and frame_idx == 0) + or self._profile_end_frame == frame_idx + ): + # Stop profiling if reached the last frame + self._stop_profiling() + + def _det_track_one_frame_impl( + self, + frame_idx: int, + num_frames: int, + reverse: bool, + input_batch: BatchedDatapoint, + geometric_prompt: Any, + tracker_states_local: List[Any], + tracker_metadata_prev: Dict[str, Any], + feature_cache: Dict, + orig_vid_height: int, + orig_vid_width: int, + is_image_only: bool, + ): + """ + This function handles one-step inference for the multiplex model in an SPMD manner. + At a high-level, all GPUs execute the same function calls as if it's done on a single GPU, + while under the hood, some function calls involve distributed computation based on sharded + SAM2 states. + + - `input_batch` contains image and other inputs on the entire video; it should be identical across GPUs + - `tracker_states_local` holds the local masklet information in this GPU shard + - `tracker_metadata_prev` manages the metadata for SAM2 objects, such as which masklet is hold on which GPUs + it contains both global and local masklet information + """ + + # Step 1: run backbone and FA in a distributed manner -- this is done via Sam3MultiplexDetector, + # a distributed FA model (assigned to `self.detector`) that shards frames in a round-robin manner. + # It returns a "det_out" dict for `frame_idx` and fills SAM2 backbone features for `frame_idx` + # into `feature_cache`. Despite its distributed inference under the hood, the results would be + # the same as if it is running backbone and FA for every frame on a single GPU. + with torch.profiler.record_function("run_backbone_and_detection"): + det_out, pos_pred_mask = self.run_backbone_and_detection( + frame_idx=frame_idx, + num_frames=num_frames, + reverse=reverse, + input_batch=input_batch, + geometric_prompt=geometric_prompt, + feature_cache=feature_cache, + use_batched_grounding=self.use_batched_grounding, + batched_grounding_batch_size=self.batched_grounding_batch_size, + ) + + # Step 2: each GPU propagates its local SAM2 states to get the SAM2 prediction masks. + # the returned `tracker_low_res_masks_global` contains the concatenated masklet predictions + # gathered from all GPUs (as if they are propagated on a single GPU). Note that this step only + # runs the SAM2 propagation step, but doesn't encode new memory for the predicted masks; + # we defer memory encoding to `run_tracker_update_execution_phase` after resolving all heuristics. + with torch.profiler.record_function("run_tracker_propagation"): + if tracker_metadata_prev == {}: + # initialize masklet metadata if it's uninitialized (empty dict) + tracker_metadata_prev.update(self._initialize_metadata()) + tracker_low_res_masks_global, tracker_obj_scores_global = ( + self.run_tracker_propagation( + frame_idx=frame_idx, + num_frames=num_frames, + reverse=reverse, + tracker_states_local=tracker_states_local, + tracker_metadata_prev=tracker_metadata_prev, + ) + ) + + with torch.profiler.record_function("GPU sync and filter"): + # Remove leading dimension (assumes batch size 1) + assert pos_pred_mask.shape[0] == 1 + pos_pred_mask = pos_pred_mask.squeeze(0) + det_out = {k: det_out[k][0] for k in det_out} + # Move detections we'll actually keep at the top for future logic + pos_pred_mask_idx = pos_pred_mask.argsort(descending=True) + pos_pred_mask = torch.index_select( + pos_pred_mask, dim=0, index=pos_pred_mask_idx + ) + det_out = { + k: torch.index_select(det_out[k], dim=0, index=pos_pred_mask_idx) + for k in det_out + } + + # Step 3: based on detection outputs and the propagated SAM2 prediction masks, we make plans + # for SAM2 masklet updates (i.e. which objects to add and remove, how to load-balance them, etc). + # We also run SAM2 memory encoder globally in this step to resolve non-overlapping constraints. + # **This step should involve all the heuristics needed for any updates.** Most of the update + # planning will be done on the master rank (GPU 0) and the resulting plan `sam2_update_plan` is + # broadcasted to other GPUs (to be executed in a distributed manner). This step also generates the + # new masklet metadata `tracker_metadata_new` (based on its previous version `tracker_metadata_prev`). + with torch.profiler.record_function("run_tracker_update_planning_phase"): + sam2_update_plan, tracker_metadata_new = ( + self.run_tracker_update_planning_phase( + frame_idx=frame_idx, + num_frames=num_frames, + reverse=reverse, + det_out=det_out, + det_keep=pos_pred_mask, + tracker_low_res_masks_global=tracker_low_res_masks_global, + tracker_obj_scores_global=tracker_obj_scores_global, + tracker_metadata_prev=tracker_metadata_prev, + tracker_states_local=tracker_states_local, + is_image_only=is_image_only, + ) + ) + + # Get reconditioning info from the update plan + reconditioned_obj_ids = sam2_update_plan.get("reconditioned_obj_ids", set()) + det_to_matched_trk_obj_ids = sam2_update_plan.get( + "det_to_matched_trk_obj_ids", {} + ) + + # Step 4: based on `sam2_update_plan`, each GPU executes the update w.r.t. its local SAM2 inference states + with torch.profiler.record_function("run_tracker_update_execution_phase"): + tracker_states_local_new = self.run_tracker_update_execution_phase( + frame_idx=frame_idx, + num_frames=num_frames, + reverse=reverse, + det_out=det_out, + tracker_states_local=tracker_states_local, + tracker_update_plan=sam2_update_plan, + tracker_metadata_new=tracker_metadata_new, + orig_vid_height=orig_vid_height, + orig_vid_width=orig_vid_width, + feature_cache=feature_cache, + ) + + # Step 5: finally, build the outputs for this frame (it only needs to be done on GPU 0 since + # only GPU 0 will send outputs to the server). + with torch.profiler.record_function("build_outputs"): + if self.rank == 0: + obj_id_to_mask = self.build_outputs( + frame_idx=frame_idx, + num_frames=num_frames, + reverse=reverse, + det_out=det_out, + tracker_low_res_masks_global=tracker_low_res_masks_global, + tracker_obj_scores_global=tracker_obj_scores_global, + tracker_metadata_prev=tracker_metadata_prev, + sam2_update_plan=sam2_update_plan, + orig_vid_height=orig_vid_height, + orig_vid_width=orig_vid_width, + reconditioned_obj_ids=reconditioned_obj_ids, + det_to_matched_trk_obj_ids=det_to_matched_trk_obj_ids, + ) + obj_id_to_score = tracker_metadata_new["obj_id_to_score"] + else: + obj_id_to_mask, obj_id_to_score = {}, {} # dummy outputs on other GPUs + # a few statistics for the current frame as a part of the output + frame_stats = { + "num_obj_tracked": np.sum(tracker_metadata_new["num_obj_per_gpu"]), + "num_obj_dropped": sam2_update_plan["num_obj_dropped_due_to_limit"], + } + # add sam2 scores to metadata, it should be fired for frames except the first frame + if tracker_obj_scores_global.shape[0] > 0: + # Convert tracker_obj_scores_global to sigmoid scores before updating + tracker_obj_scores_global = tracker_obj_scores_global.sigmoid() + sam2_obj_ids = tracker_metadata_prev["obj_ids_all_gpu"] + tracker_metadata_new["obj_id_to_sam2_score_frame_wise"][frame_idx].update( + dict(zip(sam2_obj_ids, tracker_obj_scores_global)) + ) + + return ( + obj_id_to_mask, # a dict: obj_id --> output mask + obj_id_to_score, # a dict: obj_id --> output score (prob) + tracker_states_local_new, + tracker_metadata_new, + frame_stats, + tracker_obj_scores_global, # a dict: obj_id --> sam2 frame-level scores + ) + + def run_backbone_and_detection( + self, + frame_idx: int, + num_frames: int, + input_batch: BatchedDatapoint, + geometric_prompt: Any, + feature_cache: Dict, + reverse: bool, + use_batched_grounding: bool = False, + batched_grounding_batch_size: int = 16, + ): + # Step 1: if text feature is not cached in `feature_cache`, compute and cache it + text_batch_key = tuple(input_batch.find_text_batch) + if "text" not in feature_cache or text_batch_key not in feature_cache["text"]: + text_outputs = self.detector.backbone.forward_text( + input_batch.find_text_batch, device=self.device + ) + # note: we only cache the text feature of the most recent prompt + feature_cache["text"] = {text_batch_key: text_outputs} + else: + text_outputs = feature_cache["text"][text_batch_key] + + # Step 2: run backbone, FA detection, and post-processing with NMS + # Extract max_frame_num_to_track from feature_cache if available + tracking_bounds = feature_cache.get("tracking_bounds", {}) + max_frame_num_to_track = tracking_bounds.get("max_frame_num_to_track") + start_frame_idx = tracking_bounds.get("propagate_in_video_start_frame_idx") + backbone_out = { + "img_batch_all_stages": input_batch.img_batch, + **text_outputs, + } + + if use_batched_grounding: + # Use fully batched forward_grounding approach + if "grounding_cache" not in feature_cache: + feature_cache["grounding_cache"] = {} + + with torch.profiler.record_function( + "forward_video_grounding_batched_multigpu" + ): + sam3_image_out, _ = ( + self.detector.forward_video_grounding_batched_multigpu( + backbone_out=backbone_out, + find_inputs=input_batch.find_inputs, + geometric_prompt=geometric_prompt, + frame_idx=frame_idx, + num_frames=num_frames, + grounding_cache=feature_cache["grounding_cache"], + track_in_reverse=reverse, + return_sam2_backbone_feats=True, + run_nms=self.det_nms_thresh > 0.0, + nms_prob_thresh=self.score_threshold_detection, + nms_iou_thresh=self.det_nms_thresh, + nms_use_iom=self.det_nms_use_iom, + max_frame_num_to_track=max_frame_num_to_track, + propagate_in_video_start_frame_idx=start_frame_idx, + feature_cache=feature_cache, + batch_size=batched_grounding_batch_size, + ) + ) + else: + # Use existing multi-GPU distributed approach + if "multigpu_buffer" not in feature_cache: + # "multigpu_buffer" is a buffer cache used by `self.detector` and it needs + # to be passed to `forward_video_grounding_multigpu` for every call + feature_cache["multigpu_buffer"] = {} + + with torch.profiler.record_function("forward_video_grounding_multigpu"): + sam3_image_out, _ = self.detector.forward_video_grounding_multigpu( + backbone_out=backbone_out, + find_inputs=input_batch.find_inputs, + geometric_prompt=geometric_prompt, + frame_idx=frame_idx, + num_frames=num_frames, + multigpu_buffer=feature_cache["multigpu_buffer"], + track_in_reverse=reverse, + # also get the SAM2 backbone features + return_sam2_backbone_feats=True, + # run NMS as a part of distributed FA computation + run_nms=self.det_nms_thresh > 0.0, + nms_prob_thresh=self.score_threshold_detection, + nms_iou_thresh=self.det_nms_thresh, + nms_use_iom=self.det_nms_use_iom, + # pass max_frame_num_to_track to respect tracking limits + max_frame_num_to_track=max_frame_num_to_track, + propagate_in_video_start_frame_idx=start_frame_idx, + # pass feature_cache for buffered backbone computation + feature_cache=feature_cache, + ) + + # note: detections in `sam3_image_out` has already gone through NMS + pred_probs = sam3_image_out["pred_logits"].squeeze(-1).sigmoid() + pred_boxes_xyxy = sam3_image_out["pred_boxes_xyxy"] + pred_masks = sam3_image_out["pred_masks"] + # get the positive detection outputs above threshold + pos_pred_mask = pred_probs > self.score_threshold_detection + + if self.suppress_det_close_to_boundary: + # Suppress detections too close to image edges (for normalized boxes). + keep = self._suppress_detections_close_to_boundary(pred_boxes_xyxy) + pos_pred_mask = pos_pred_mask & keep + + det_out = { + "bbox": pred_boxes_xyxy, + "mask": pred_masks, + "scores": pred_probs, + } + + # Step 3: build SAM2 backbone features and store them in `feature_cache` + backbone_cache = {} + if self.is_multiplex: + # For the multiplex model we have separate interaction and propagation features + # TODO: We do not need the interaction features every frame so there are rooms for optimization + interaction_sam_mask_decoder = self.tracker.interactive_sam_mask_decoder + interaction_backbone_fpn = [ + interaction_sam_mask_decoder.conv_s0( + sam3_image_out["interactive_backbone_fpn_0"] + ), + interaction_sam_mask_decoder.conv_s1( + sam3_image_out["interactive_backbone_fpn_1"] + ), + sam3_image_out[ + "interactive_backbone_fpn_2" + ], # fpn_2 doesn't need additional conv + ] + interaction_backbone_out = { + "vision_features": interaction_backbone_fpn[-1], # top-level feature + "vision_mask": None, + "vision_pos_enc": sam3_image_out["interactive_backbone_pos_enc"], + "backbone_fpn": [ + NestedTensor(x, None) for x in interaction_backbone_fpn + ], + } + backbone_cache["interactive"] = interaction_backbone_out + sam_mask_decoder = self.tracker.sam_mask_decoder + sam2_backbone_fpn = [ + sam_mask_decoder.conv_s0(sam3_image_out["sam2_backbone_fpn_0"]), + sam_mask_decoder.conv_s1(sam3_image_out["sam2_backbone_fpn_1"]), + sam3_image_out["sam2_backbone_fpn_2"], # fpn_2 doesn't need additional conv + ] + sam2_backbone_out = { + "vision_features": sam2_backbone_fpn[-1], # top-level feature + "vision_mask": None, + "vision_pos_enc": sam3_image_out["sam2_backbone_pos_enc"], + "backbone_fpn": [NestedTensor(x, None) for x in sam2_backbone_fpn], + } + backbone_cache["sam2_backbone_out"] = sam2_backbone_out + + with torch.profiler.record_function("run_backbone_and_detection.feature_cache"): + feature_cache[frame_idx] = ( + input_batch.img_batch.tensors[frame_idx], + backbone_cache, + ) + # remove from `feature_cache` old features to save GPU memory + feature_cache.pop(frame_idx - 1 if not reverse else frame_idx + 1, None) + return det_out, pos_pred_mask + + def run_tracker_propagation( + self, + frame_idx: int, + num_frames: int, + reverse: bool, + tracker_states_local: List[Any], + tracker_metadata_prev: Dict[str, np.ndarray], + ): + # Step 1: propagate the local SAM2 states to get the current frame's prediction + # `low_res_masks_local` of the existing masklets on this GPU + # - obj_ids_local: List[int] -- list of object IDs + # - low_res_masks_local: Tensor -- (num_local_obj, H_mask, W_mask) + with torch.profiler.record_function("propagate_tracker_one_frame_local_gpu"): + obj_ids_local, low_res_masks_local, obj_scores_local = ( + self._propogate_tracker_one_frame_local_gpu( + tracker_states_local, frame_idx=frame_idx, reverse=reverse + ) + ) + + assert np.all( + obj_ids_local == tracker_metadata_prev["obj_ids_per_gpu"][self.rank] + ), "{} != {}".format( + obj_ids_local, tracker_metadata_prev["obj_ids_per_gpu"][self.rank] + ) + + # Step 2: all-gather `low_res_masks_local` into `low_res_masks_global` + # - low_res_masks_global: Tensor -- (num_global_obj, H_mask, W_mask) + with torch.profiler.record_function("all_gather_low_res_masks_local"): + _, H_mask, W_mask = low_res_masks_local.shape + if self.world_size > 1: + # `low_res_masks_local` and `obj_scores_local` need to be contiguous and float32 + # (they could be non-contiguous due to slicing and/or bfloat16 due to autocast) + low_res_masks_local = low_res_masks_local.float().contiguous() + obj_scores_local = obj_scores_local.float().contiguous() + num_obj_this_gpu = tracker_metadata_prev["num_obj_per_gpu"][self.rank] + assert low_res_masks_local.size(0) == num_obj_this_gpu + assert obj_scores_local.size(0) == num_obj_this_gpu + low_res_masks_peers = [ + low_res_masks_local.new_empty(num_obj, H_mask, W_mask) + for num_obj in tracker_metadata_prev["num_obj_per_gpu"] + ] + obj_scores_peers = [ + obj_scores_local.new_empty(num_obj) + for num_obj in tracker_metadata_prev["num_obj_per_gpu"] + ] + dist.all_gather(low_res_masks_peers, low_res_masks_local) + dist.all_gather(obj_scores_peers, obj_scores_local) + low_res_masks_global = torch.cat(low_res_masks_peers, dim=0) + obj_scores_global = torch.cat(obj_scores_peers, dim=0) + else: + low_res_masks_global = low_res_masks_local + obj_scores_global = obj_scores_local + return low_res_masks_global, obj_scores_global + + def _recondition_masklets( + self, + frame_idx, + det_out: Dict[str, Tensor], + trk_id_to_max_iou_high_conf_det: Dict[int, int], # trk_obj_id -> det_idx + tracker_states_local: List[Any], + tracker_metadata: Dict[str, np.ndarray], + tracker_obj_scores_global: Tensor, + tracker_low_res_masks_global: Tensor, + ): + reconditioned_obj_ids = set() + HIGH_CONF_THRESH = 0.8 + input_mask_res = self.tracker.input_mask_size + + if len(trk_id_to_max_iou_high_conf_det) == 0: + return tracker_states_local, reconditioned_obj_ids + + # === BATCH ALL INDEX LOOKUPS ON GPU === + trk_obj_ids = list(trk_id_to_max_iou_high_conf_det.keys()) + det_indices = list(trk_id_to_max_iou_high_conf_det.values()) + + # Convert obj_ids_all_gpu to tensor once (keep on GPU) + obj_ids_all_gpu_t = torch.from_numpy(tracker_metadata["obj_ids_all_gpu"]).to( + device=tracker_obj_scores_global.device + ) + trk_obj_ids_t = torch.tensor( + trk_obj_ids, device=tracker_obj_scores_global.device + ) + det_indices_t = torch.tensor( + det_indices, device=tracker_obj_scores_global.device + ) + + # Batched lookup: find obj_idx for each trk_obj_id + # Shape: (num_trk, num_all_obj) -> find matching indices + matches = trk_obj_ids_t.unsqueeze(1) == obj_ids_all_gpu_t.unsqueeze(0) # (N, M) + obj_indices_t = matches.int().argmax(dim=1) # (N,) + + # Batched score lookup and filtering - NO SYNC until we need CPU decision + obj_scores_batch = tracker_obj_scores_global[obj_indices_t].sigmoid() # (N,) + high_conf_mask = obj_scores_batch > HIGH_CONF_THRESH # (N,) bool tensor on GPU + + # === SINGLE SYNC POINT: Transfer filter mask to CPU === + high_conf_mask_cpu = high_conf_mask.cpu().numpy() + + # Filter to only high-confidence items + valid_trk_obj_ids = [ + tid for tid, valid in zip(trk_obj_ids, high_conf_mask_cpu) if valid + ] + valid_det_indices = [ + did for did, valid in zip(det_indices, high_conf_mask_cpu) if valid + ] + valid_obj_indices = obj_indices_t[high_conf_mask] # Keep as tensor + + if len(valid_trk_obj_ids) == 0: + return tracker_states_local, reconditioned_obj_ids + + # === BATCH MASK OPERATIONS === + valid_det_indices_t = torch.tensor( + valid_det_indices, device=det_out["mask"].device + ) + + # Batch fetch all detection masks at once + new_masks = det_out["mask"][valid_det_indices_t] # (K, H, W) + new_masks_binary = ( + F.interpolate( + new_masks.unsqueeze(1), + size=(input_mask_res, input_mask_res), + mode="bilinear", + align_corners=False, + ).squeeze(1) + > 0 + ) # (K, H, W) + + # Batch update low_res_masks_global + old_masks = tracker_low_res_masks_global[valid_obj_indices] # (K, H, W) + binary_agreement = (new_masks > 0) == (old_masks > 0) + updated_masks = torch.where(binary_agreement, old_masks, new_masks) + + # Batch hole filling + updated_masks = fill_holes_in_mask_scores( + updated_masks.unsqueeze(1), + fill_hole_area=self.fill_hole_area, + sprinkle_removal_area=self.sprinkle_removal_area, + fill_holes=True, + remove_sprinkles=True, + ).squeeze(1) + + # Write back (scatter) + tracker_low_res_masks_global[valid_obj_indices] = updated_masks + + # === NOW DO THE STATE UPDATES (still needs iteration but with pre-filtered data) === + if self.is_multiplex: + state_to_recondition_info = {} + for i, trk_obj_id in enumerate(valid_trk_obj_ids): + for state_idx, inference_state in enumerate(tracker_states_local): + if trk_obj_id in inference_state["obj_ids"]: + if state_idx not in state_to_recondition_info: + state_to_recondition_info[state_idx] = [] + state_to_recondition_info[state_idx].append( + (trk_obj_id, new_masks_binary[i]) + ) + break + + for state_idx, recondition_list in state_to_recondition_info.items(): + inference_state = tracker_states_local[state_idx] + obj_ids_to_recondition = [item[0] for item in recondition_list] + masks_to_recondition = torch.stack( + [item[1] for item in recondition_list] + ) + with torch.profiler.record_function( + "_recodition_masklets.add_new_masks" + ): + self.tracker.add_new_masks( + inference_state=inference_state, + frame_idx=frame_idx, + obj_ids=obj_ids_to_recondition, + masks=masks_to_recondition, + reconditioning=True, + ) + reconditioned_obj_ids.update(inference_state["obj_idx_to_id"].values()) + else: + # Non-multiplex: still iterate but masks already computed + for i, trk_obj_id in enumerate(valid_trk_obj_ids): + for inference_state in tracker_states_local: + if trk_obj_id in inference_state["obj_ids"]: + self.tracker.add_new_mask( + inference_state=inference_state, + frame_idx=frame_idx, + obj_id=trk_obj_id, + mask=new_masks_binary[i], + ) + reconditioned_obj_ids.update( + inference_state["obj_idx_to_id"].values() + ) + break + + return tracker_states_local, reconditioned_obj_ids + + def _deepcopy(self, x): + # If running in prod, dont need to do a deepcopy as we only traverse in 1 direction + if True: + return x + return deepcopy(x) + + def run_tracker_update_planning_phase( + self, + frame_idx: int, + num_frames: int, + reverse: bool, + det_out: Dict[str, Tensor], + det_keep: Tensor, + tracker_low_res_masks_global: Tensor, + tracker_obj_scores_global: Tensor, + tracker_metadata_prev: Dict[str, np.ndarray], + tracker_states_local: List[Any], + is_image_only: bool = False, + ): + # initialize new metadata from previous metadata (its values will be updated later) + with torch.profiler.record_function("initialize_tracker_metadata_new"): + tracker_metadata_new = self._create_planning_metadata(tracker_metadata_prev) + + # Initialize reconditioned_obj_ids early to avoid UnboundLocalError + reconditioned_obj_ids = set() + + # Step 1: make the update plan and resolve heuristics on GPU 0 + det_mask_preds: Tensor = det_out["mask"] # low-res mask logits + det_scores: Tensor = det_out["scores"].float() + # a) match FA and SAM2 masks and find new objects + with torch.profiler.record_function("associate_det_trk"): + adt_result = self._associate_det_trk( + det_masks=det_mask_preds, + det_scores=det_scores, + det_keep=det_keep, + trk_masks=tracker_low_res_masks_global, + trk_obj_ids=tracker_metadata_prev["obj_ids_all_gpu"], + default_det_thresh=( + self.image_only_det_thresh if is_image_only else None + ), + ) + + # b) handle hotstart heuristics to remove objects (GPU-vectorized, no sync!) + # here `rank0_metadata` contains metadata stored on (and only accessible to) GPU 0; + # we avoid broadcasting them to other GPUs to save communication cost, assuming + # that `rank0_metadata` is not needed by other GPUs + rank0_metadata_new = self._deepcopy(tracker_metadata_prev["rank0_metadata"]) + if not hasattr(self, "_warm_up_complete") or self._warm_up_complete: + # Call GPU-vectorized hotstart using lazy adt_result (NO realize_adt yet!) + with torch.profiler.record_function("_process_hotstart_gpu"): + to_remove_mask, to_suppress_mask, gpu_metadata_new = ( + self._process_hotstart_gpu( + frame_idx=frame_idx, + reverse=reverse, + adt_result=adt_result, # Still lazy - no sync! + tracker_metadata_prev=tracker_metadata_prev, + gpu_metadata_prev=tracker_metadata_prev["gpu_metadata"], + ) + ) + # IMPORTANT: From this point, tracker_metadata_new["gpu_metadata"] is updated but CPU metadata (obj_ids_all_gpu, etc.) is NOT + tracker_metadata_new["gpu_metadata"] = gpu_metadata_new + else: + # if warm-up is not complete, we don't remove any objects + N_obj = tracker_low_res_masks_global.size(0) + to_remove_mask = torch.zeros( + N_obj, dtype=torch.bool, device=tracker_low_res_masks_global.device + ) + to_suppress_mask = torch.zeros( + N_obj, dtype=torch.bool, device=tracker_low_res_masks_global.device + ) + tracker_metadata_new["rank0_metadata"] = rank0_metadata_new + + # Step 3 (optional): recondition masklets based on high-confidence detections before memory encoding + # NOTE: Running this in execution phase (after memory encoding) can lead to suboptimal results + should_recondition_iou = False + + # Evaluate tracklets for reconditioning based on bbox IoU mismatch with detections + if self.reconstruction_bbox_iou_thresh > 0: + adt_result = realize_adt_result( + adt_result, tracker_metadata_prev, det_mask_preds + ) + if ( + self.reconstruction_bbox_iou_thresh > 0 + and len(adt_result.trk_id_to_max_iou_high_conf_det) > 0 + ): + with torch.profiler.record_function( + "evaluate_reconstruction_bbox_iou_thresh" + ): + trk_obj_ids = adt_result.trk_id_to_max_iou_high_conf_det.keys() + sam2_obj_ids_all_gpu = list(tracker_metadata_prev["obj_ids_all_gpu"]) + trk_ids = [ + sam2_obj_ids_all_gpu.index(trk_obj_id) + for trk_obj_id in trk_obj_ids + if trk_obj_id in sam2_obj_ids_all_gpu + ] + det_ids = list(adt_result.trk_id_to_max_iou_high_conf_det.values()) + + det_boxes_bbox_iou = det_out["bbox"][det_ids] + det_scores_bbox_iou = det_out["scores"][det_ids] + sam2_mask = tracker_low_res_masks_global[trk_ids] + mask_binary = sam2_mask > 0 + sam2_box_pixels = mask_to_box(mask_binary.unsqueeze(1)).squeeze(1) + mask_height, mask_width = sam2_mask.shape[-2:] + sam2_box_normalized = sam2_box_pixels / torch.tensor( + [mask_width, mask_height, mask_width, mask_height], + device=sam2_box_pixels.device, + ) + iou = fast_diag_box_iou(det_boxes_bbox_iou, sam2_box_normalized)[0] + if iou < self.reconstruction_bbox_iou_thresh and torch.any( + det_scores_bbox_iou >= self.reconstruction_bbox_det_score + ): + should_recondition_iou = True + + if ( + self.recondition_every_nth_frame > 0 + and frame_idx % self.recondition_every_nth_frame == 0 + ): + adt_result = realize_adt_result( + adt_result, tracker_metadata_prev, det_mask_preds + ) + + should_recondition_periodic = ( + self.recondition_every_nth_frame > 0 + and frame_idx % self.recondition_every_nth_frame == 0 + and len(adt_result.trk_id_to_max_iou_high_conf_det) > 0 + ) + + # Recondition if periodic or IoU condition met + if should_recondition_periodic or should_recondition_iou: + adt_result = realize_adt_result( + adt_result, tracker_metadata_prev, det_mask_preds + ) + # NOTE: sam2_low_res_mask_global is modified in-place on all GPUs. + with torch.profiler.record_function("_recondition_masklets"): + tracker_states_local, reconditioned_obj_ids = ( + self._recondition_masklets( + frame_idx, + det_out, + adt_result.trk_id_to_max_iou_high_conf_det, + tracker_states_local, + tracker_metadata_prev, + tracker_obj_scores_global, + tracker_low_res_masks_global, + ) + ) + + for state in tracker_states_local: + if any( + obj_id in reconditioned_obj_ids + for obj_id in state.get("obj_ids", []) + ): + self.tracker.propagate_in_video_preflight( + state, run_mem_encoder=True + ) + + # Step 4: Run SAM2 memory encoder on the current frame's prediction masks + # This is done on all GPUs + batch_size = tracker_low_res_masks_global.size(0) + if batch_size > 0: + if not hasattr(self, "_warm_up_complete") or self._warm_up_complete: + if self.suppress_overlapping_based_on_recent_occlusion_threshold > 0.0: + # NOTE: tracker_low_res_masks_global is updated in-place then returned + with torch.profiler.record_function( + "_suppress_overlapping_based_on_recent_occlusion" + ): + tracker_low_res_masks_global = ( + self._suppress_overlapping_based_on_recent_occlusion( + frame_idx, + tracker_low_res_masks_global, + tracker_metadata_prev, + tracker_metadata_new, + to_remove_mask, # GPU boolean mask, no sync! + reverse, + ) + ) + with torch.profiler.record_function("_tracker_update_memories"): + self._tracker_update_memories( + tracker_states_local, + frame_idx, + tracker_metadata=tracker_metadata_prev, + low_res_masks=tracker_low_res_masks_global, + ) + + # NOW realize adt_result after memory encoding (sync only for GPU load balancing) + adt_result = realize_adt_result( + adt_result, tracker_metadata_prev, det_mask_preds + ) + new_det_obj_ids, new_det_gpu_ids, num_obj_dropped_due_to_limit = ( + adt_result.get_new_det_gpu_ids( + tracker_metadata_prev, is_image_only, det_scores, self + ) + ) + + # Convert GPU removal mask to CPU obj_id set for metadata updates + if not hasattr(self, "_warm_up_complete") or self._warm_up_complete: + obj_ids_all_gpu = tracker_metadata_prev["obj_ids_all_gpu"] + to_remove_cpu = to_remove_mask.cpu().numpy() + obj_ids_newly_removed = set(obj_ids_all_gpu[to_remove_cpu].tolist()) + else: + obj_ids_newly_removed = set() + + # Step 4: update the SAM2 metadata based on the update plan + # note: except for "rank0_metadata" (that is only available on GPU 0), + # the updated `tracker_metadata_new` should be identical on all GPUs + for rank in range(self.world_size): + new_det_obj_ids_this_gpu = new_det_obj_ids[new_det_gpu_ids == rank] + updated_obj_ids_this_gpu = tracker_metadata_new["obj_ids_per_gpu"][rank] + if len(new_det_obj_ids_this_gpu) > 0: + updated_obj_ids_this_gpu = np.concatenate( + [updated_obj_ids_this_gpu, new_det_obj_ids_this_gpu] + ) + if len(obj_ids_newly_removed) > 0: + is_removed = np.isin( + updated_obj_ids_this_gpu, list(obj_ids_newly_removed) + ) + updated_obj_ids_this_gpu = updated_obj_ids_this_gpu[~is_removed] + tracker_metadata_new["obj_ids_per_gpu"][rank] = updated_obj_ids_this_gpu + tracker_metadata_new["num_obj_per_gpu"][rank] = len( + updated_obj_ids_this_gpu + ) + tracker_metadata_new["obj_ids_all_gpu"] = np.concatenate( + tracker_metadata_new["obj_ids_per_gpu"] + ) + # update object scores and the maximum object ID assigned so far + if len(new_det_obj_ids) > 0: + det_scores_np: np.ndarray = det_scores.cpu().numpy() + tracker_metadata_new["obj_id_to_score"].update( + zip(new_det_obj_ids, det_scores_np[adt_result.new_det_fa_inds]) + ) + # sam2 scores are not available for new objects, use det score instead. + # Store as GPU tensors for consistency with SAM2 propagation scores + new_det_scores_tensor = det_scores[adt_result.new_det_fa_inds] + tracker_metadata_new["obj_id_to_sam2_score_frame_wise"][frame_idx].update( + zip(new_det_obj_ids, new_det_scores_tensor) + ) + tracker_metadata_new["max_obj_id"] = max( + tracker_metadata_new["max_obj_id"], + np.max(new_det_obj_ids), + ) + # for removed objects, we set their scores to a very low value (-1e4) but still + # keep them in "obj_id_to_score" (it's easier to handle outputs this way) + for obj_id in obj_ids_newly_removed: + tracker_metadata_new["obj_id_to_score"][obj_id] = -1e4 + # Store as GPU tensor for consistency + tracker_metadata_new["obj_id_to_sam2_score_frame_wise"][frame_idx][ + obj_id + ] = torch.tensor(-1e4, dtype=torch.float32, device=det_scores.device) + tracker_metadata_new["obj_id_to_last_occluded"].pop(obj_id, None) + # check that "rank0_metadata" is in tracker_metadata_new if and only if it's GPU 0 + assert "rank0_metadata" in tracker_metadata_new + if self.masklet_confirmation_enable: + with torch.profiler.record_function("update_masklet_confirmation_status"): + rank0_metadata = self.update_masklet_confirmation_status( + rank0_metadata=tracker_metadata_new["rank0_metadata"], + obj_ids_all_gpu_prev=tracker_metadata_prev["obj_ids_all_gpu"], + obj_ids_all_gpu_updated=tracker_metadata_new["obj_ids_all_gpu"], + det_to_matched_trk_obj_ids=adt_result.det_to_matched_trk_obj_ids, + new_det_obj_ids=new_det_obj_ids, + ) + tracker_metadata_new["rank0_metadata"] = rank0_metadata + + # Compact GPU metadata NOW (after sync) in preparation for next frame + # This removes entries for objects that will be deleted in execution phase + # so next frame's _process_hotstart_gpu doesn't need to do sync-inducing compaction + if not hasattr(self, "_warm_up_complete") or self._warm_up_complete: + if ( + "gpu_metadata" in tracker_metadata_new + and tracker_metadata_new["gpu_metadata"].get("N_obj", 0) > 0 + ): + with torch.profiler.record_function("compact_gpu_metadata"): + gpu_meta = tracker_metadata_new["gpu_metadata"] + removed_mask = gpu_meta[ + "removed_mask" + ] # (N_obj,) - which objects marked for removal + keep_indices = torch.nonzero(~removed_mask, as_tuple=True)[0] + + gpu_meta["obj_first_frame"] = gpu_meta["obj_first_frame"][ + keep_indices + ] + gpu_meta["consecutive_unmatch_count"] = gpu_meta[ + "consecutive_unmatch_count" + ][keep_indices] + gpu_meta["trk_keep_alive"] = gpu_meta["trk_keep_alive"][ + keep_indices + ] + gpu_meta["removed_mask"] = gpu_meta["removed_mask"][ + keep_indices + ] # Should be all False + gpu_meta["last_occluded_tensor"] = gpu_meta["last_occluded_tensor"][ + keep_indices + ] + + # Compact pairwise matrix (remove both rows and columns) + overlap_counts = gpu_meta["overlap_pair_counts"] + overlap_counts = overlap_counts[keep_indices][:, keep_indices] + gpu_meta["overlap_pair_counts"] = overlap_counts + + # Update N_obj to reflect post-removal count + gpu_meta["N_obj"] = keep_indices.size(0) + + # After compaction, extend gpu_metadata with new objects' initial values + # This ensures obj_first_frame is set to the detection frame, not propagation frame + num_new = len(new_det_obj_ids) + if num_new > 0: + with torch.profiler.record_function( + "extend_gpu_metadata_for_new_objects" + ): + gpu_meta = tracker_metadata_new["gpu_metadata"] + device = det_scores.device + NEVER_OCCLUDED = -1 + + # Extend all metadata tensors for new objects + gpu_meta["obj_first_frame"] = torch.cat( + [ + gpu_meta.get( + "obj_first_frame", + torch.empty(0, dtype=torch.long, device=device), + ), + torch.full( + (num_new,), frame_idx, dtype=torch.long, device=device + ), + ] + ) + gpu_meta["consecutive_unmatch_count"] = torch.cat( + [ + gpu_meta.get( + "consecutive_unmatch_count", + torch.empty(0, dtype=torch.long, device=device), + ), + torch.zeros(num_new, dtype=torch.long, device=device), + ] + ) + gpu_meta["trk_keep_alive"] = torch.cat( + [ + gpu_meta.get( + "trk_keep_alive", + torch.empty(0, dtype=torch.long, device=device), + ), + torch.full( + (num_new,), + self.init_trk_keep_alive, + dtype=torch.long, + device=device, + ), + ] + ) + gpu_meta["removed_mask"] = torch.cat( + [ + gpu_meta.get( + "removed_mask", + torch.empty(0, dtype=torch.bool, device=device), + ), + torch.zeros(num_new, dtype=torch.bool, device=device), + ] + ) + gpu_meta["last_occluded_tensor"] = torch.cat( + [ + gpu_meta.get( + "last_occluded_tensor", + torch.empty(0, dtype=torch.long, device=device), + ), + torch.full( + (num_new,), + NEVER_OCCLUDED, + dtype=torch.long, + device=device, + ), + ] + ) + + # Grow overlap matrix + old_N = gpu_meta.get("N_obj", 0) + new_N = old_N + num_new + old_overlap = gpu_meta.get( + "overlap_pair_counts", + torch.zeros((0, 0), dtype=torch.long, device=device), + ) + new_overlap = torch.zeros( + (new_N, new_N), dtype=torch.long, device=device + ) + if old_N > 0: + new_overlap[:old_N, :old_N] = old_overlap + gpu_meta["overlap_pair_counts"] = new_overlap + + gpu_meta["N_obj"] = new_N + + sam2_update_plan = { + "new_det_fa_inds": adt_result.new_det_fa_inds, # np.ndarray + "new_det_obj_ids": new_det_obj_ids, # np.ndarray + "new_det_gpu_ids": new_det_gpu_ids, # np.ndarray + "unmatched_trk_obj_ids": adt_result.unmatched_trk_obj_ids, # np.ndarray + "det_to_matched_trk_obj_ids": adt_result.det_to_matched_trk_obj_ids, # dict + "obj_ids_newly_removed": obj_ids_newly_removed, # set + "num_obj_dropped_due_to_limit": num_obj_dropped_due_to_limit, # int + "trk_id_to_max_iou_high_conf_det": adt_result.trk_id_to_max_iou_high_conf_det, # dict + "reconditioned_obj_ids": reconditioned_obj_ids, # set + } + return sam2_update_plan, tracker_metadata_new + + def _suppress_overlapping_based_on_recent_occlusion( + self, + frame_idx: int, + tracker_low_res_masks_global: Tensor, + tracker_metadata_prev: Dict[str, Any], + tracker_metadata_new: Dict[str, Any], + to_remove_mask: Tensor, # GPU boolean mask (N_obj,) instead of CPU set + reverse: bool = False, + ): + """ + Suppress overlapping masks based on the most recent occlusion information. If an object is removed by hotstart, we always suppress it if it overlaps with any other object. + Args: + frame_idx (int): The current frame index. + tracker_low_res_masks_global (Tensor): The low-resolution masks for the current frame. + tracker_metadata_prev (Dict[str, Any]): The metadata from the previous frame. + tracker_metadata_new (Dict[str, Any]): The metadata for the current frame (with updated gpu_metadata from _process_hotstart_gpu). + to_remove_mask (Tensor): GPU boolean mask (N_obj,) indicating which objects are removed. + Return: + Tensor: The updated low-resolution masks with some objects suppressed. + """ + # NOTE: obj_ids_global is only used for debug logging, so we can use prev (it won't match perfectly but close enough for debugging) + # The actual suppression logic uses GPU tensors which ARE in the correct index space from tracker_metadata_new + obj_ids_global = tracker_metadata_prev["obj_ids_all_gpu"] + binary_tracker_low_res_masks_global = tracker_low_res_masks_global > 0 + batch_size = tracker_low_res_masks_global.size(0) + num_ids = len(obj_ids_global) + + # immediately to force proper debugging. (Aligned with merge decision 4.5.2) + assert batch_size == num_ids, ( + f"Mask/metadata count mismatch in _suppress_overlapping: " + f"batch_size={batch_size}, num_ids={num_ids}, frame_idx={frame_idx}" + ) + + binary_tracker_low_res_masks_global = tracker_low_res_masks_global > 0 + if batch_size > 0: + assert ( + len(obj_ids_global) == batch_size + ), f"Mismatch in number of objects: {len(obj_ids_global)} vs {batch_size}" + NEVER_OCCLUDED = -1 + ALWAYS_OCCLUDED = 100000 # This value should be larger than any possible frame index, indicates that the object was removed by hotstart logic + + # GPU-vectorized: Build last_occluded_prev tensor without iteration/syncs + device = binary_tracker_low_res_masks_global.device + + # Get last_occluded from UPDATED gpu_metadata (already in correct index space from _process_hotstart_gpu) + gpu_metadata_new = tracker_metadata_new["gpu_metadata"] + last_occluded_prev = gpu_metadata_new["last_occluded_tensor"] + + # Sanity check: ensure last_occluded_tensor is in sync with batch_size + assert last_occluded_prev.size(0) == batch_size, ( + f"last_occluded_tensor size mismatch: {last_occluded_prev.size(0)} vs {batch_size}. " + f"This indicates gpu_metadata tensors are out of sync." + ) + + # Set ALWAYS_OCCLUDED for removed objects (fully vectorized, no sync!) + last_occluded_prev = torch.where( + to_remove_mask, + torch.full_like(last_occluded_prev, ALWAYS_OCCLUDED), + last_occluded_prev, + ) + + to_suppress = self._get_objects_to_suppress_based_on_most_recently_occluded( + binary_tracker_low_res_masks_global, + last_occluded_prev, + obj_ids_global, + frame_idx, + reverse, + ) + + # Update metadata with occlusion information (fully vectorized) + is_obj_occluded = ~(binary_tracker_low_res_masks_global.any(dim=(-1, -2))) + is_obj_occluded_or_suppressed = is_obj_occluded | to_suppress + last_occluded_new = last_occluded_prev.clone() + last_occluded_new[is_obj_occluded_or_suppressed] = frame_idx + + # Store in gpu_metadata to keep it aligned with other metadata tensors + tracker_metadata_new["gpu_metadata"]["last_occluded_tensor"] = ( + last_occluded_new + ) + + # Also maintain legacy dict format for backwards compatibility + # This conversion happens on CPU AFTER memory encoding, not in critical path + tracker_metadata_new[ + "obj_id_to_last_occluded" + ] = {} # Will be populated later if needed + + # Zero out suppressed masks before memory encoding + NO_OBJ_LOGIT = -10 + tracker_low_res_masks_global[to_suppress] = NO_OBJ_LOGIT + + return tracker_low_res_masks_global + + def _create_planning_metadata(self, tracker_metadata_prev): + """Extend planning metadata with multiplex-specific fields.""" + metadata = super()._create_planning_metadata(tracker_metadata_prev) + if self.is_multiplex: + metadata["num_buc_per_gpu"] = self._deepcopy( + tracker_metadata_prev["num_buc_per_gpu"] + ) + metadata["gpu_metadata"] = tracker_metadata_prev["gpu_metadata"] + return metadata + + def _post_execution_phase_hook(self, tracker_states_local, tracker_metadata_new): + """Update bucket count after execution phase (multiplex-specific).""" + if self.is_multiplex and tracker_metadata_new is not None: + actual_bucket_count = self._count_buckets_in_states(tracker_states_local) + tracker_metadata_new["num_buc_per_gpu"][self.rank] = actual_bucket_count + + def _count_buckets_in_states(self, tracker_states_local: List[Any]) -> int: + """Count the total number of buckets across all states.""" + if not self.is_multiplex: + return 0 + total_buckets = 0 + for state in tracker_states_local: + if "multiplex_state" in state: + total_buckets += state["multiplex_state"].num_buckets + return total_buckets + + def build_outputs( + self, + frame_idx: int, + num_frames: int, + reverse: bool, + det_out: Dict[ + str, Tensor + ], # TODO: Only det_out["mask"][new_det_fa_inds_local_t] is needed + tracker_low_res_masks_global: Tensor, + tracker_obj_scores_global: Tensor, + tracker_metadata_prev: Dict[str, np.ndarray], + sam2_update_plan: Dict[str, np.ndarray], + orig_vid_height: int, + orig_vid_width: int, + reconditioned_obj_ids: set = None, + det_to_matched_trk_obj_ids: dict = None, + ): + new_det_fa_inds: np.ndarray = sam2_update_plan["new_det_fa_inds"] + new_det_obj_ids: np.ndarray = sam2_update_plan["new_det_obj_ids"] + obj_id_to_mask = {} # obj_id --> output mask tensor + + # Part 1: masks from previous SAM2 propagation + # Align IDs and masks from previous SAM2 propagation + existing_masklet_obj_ids_all = tracker_metadata_prev["obj_ids_all_gpu"] + existing_masklet_obj_ids_per_gpu = np.concatenate( + tracker_metadata_prev["obj_ids_per_gpu"] + ) + use_per_gpu_ids = len(existing_masklet_obj_ids_per_gpu) != len( + existing_masklet_obj_ids_all + ) or not np.array_equal( + existing_masklet_obj_ids_per_gpu, existing_masklet_obj_ids_all + ) + existing_masklet_obj_ids = ( + existing_masklet_obj_ids_per_gpu + if use_per_gpu_ids + else existing_masklet_obj_ids_all + ) + existing_masklet_video_res_masks = F.interpolate( + tracker_low_res_masks_global.unsqueeze(1), + size=(orig_vid_height, orig_vid_width), + mode="bilinear", + align_corners=False, + ) # (num_obj, 1, H_video, W_video) + # Pad/truncate masks to match metadata count + num_masks = existing_masklet_video_res_masks.size(0) + num_ids = len(existing_masklet_obj_ids) + if num_masks != num_ids: + if num_masks < num_ids: + pad = existing_masklet_video_res_masks.new_zeros( + (num_ids - num_masks, 1, orig_vid_height, orig_vid_width) + ) + existing_masklet_video_res_masks = torch.cat( + [existing_masklet_video_res_masks, pad], dim=0 + ) + else: + existing_masklet_video_res_masks = existing_masklet_video_res_masks[ + :num_ids + ] + existing_masklet_binary = existing_masklet_video_res_masks > 0 + for obj_id, mask in zip(existing_masklet_obj_ids, existing_masklet_binary): + obj_id_to_mask[obj_id] = mask # (1, H_video, W_video) + + # Part 2: masks from new detections + new_det_fa_inds_t = torch.from_numpy(new_det_fa_inds) + new_det_low_res_masks = det_out["mask"][new_det_fa_inds_t].unsqueeze(1) + new_det_low_res_masks = fill_holes_in_mask_scores( + new_det_low_res_masks, + fill_hole_area=self.fill_hole_area, + sprinkle_removal_area=self.sprinkle_removal_area, + fill_holes=True, + remove_sprinkles=True, + ) + new_masklet_video_res_masks = F.interpolate( + new_det_low_res_masks, + size=(orig_vid_height, orig_vid_width), + mode="bilinear", + align_corners=False, + ) # (num_obj, 1, H_video, W_video) + + new_masklet_binary = new_masklet_video_res_masks > 0 + assert len(new_det_obj_ids) == len(new_masklet_video_res_masks) + for obj_id, mask in zip(new_det_obj_ids, new_masklet_binary): + obj_id_to_mask[obj_id] = mask # (1, H_video, W_video) + + return obj_id_to_mask + + def _get_objects_to_suppress_based_on_most_recently_occluded( + self, + binary_low_res_masks: Tensor, + last_occluded: Tensor, # GPU tensor (N_obj,) with frame indices + obj_ids: np.ndarray, # numpy array of object IDs + frame_idx: int = None, + reverse: bool = False, + ): + # Suppress overlapping masks for objects that were most recently occluded + assert ( + binary_low_res_masks.dtype == torch.bool + ), f"Expected boolean tensor, got {binary_low_res_masks.dtype}" + to_suppress = torch.zeros( + binary_low_res_masks.size(0), + device=binary_low_res_masks.device, + dtype=torch.bool, + ) + if len(obj_ids) <= 1: + return to_suppress + + iou = mask_iou(binary_low_res_masks, binary_low_res_masks) # [N,N] + + # Create masks for upper triangular matrix (i < j) and IoU threshold + mask_iou_thresh = ( + iou >= self.suppress_overlapping_based_on_recent_occlusion_threshold + ) + overlapping_pairs = torch.triu(mask_iou_thresh, diagonal=1) # [N,N] + + last_occ_expanded_i = last_occluded.unsqueeze(1) # (N, 1) + last_occ_expanded_j = last_occluded.unsqueeze(0) # (1, N) + cmp_op = torch.gt if not reverse else torch.lt + + if self.allow_unoccluded_to_suppress: + # Suppress most recently occluded + suppress_i_mask = overlapping_pairs & cmp_op( + last_occ_expanded_i, last_occ_expanded_j + ) + + suppress_j_mask = overlapping_pairs & cmp_op( + last_occ_expanded_j, last_occ_expanded_i + ) + else: + # Suppress most recently occluded + suppress_i_mask = ( + overlapping_pairs + & cmp_op( + last_occ_expanded_i, last_occ_expanded_j + ) # (last_occ_expanded_i > last_occ_expanded_j) + & (last_occ_expanded_j > -1) + # j can suppress i only if j was previously occluded + ) + + suppress_j_mask = ( + overlapping_pairs + & cmp_op(last_occ_expanded_j, last_occ_expanded_i) + & ( + last_occ_expanded_i > -1 + ) # i can suppress j only if i was previously occluded + ) + + # Apply suppression + to_suppress = suppress_i_mask.any(dim=1) | suppress_j_mask.any(dim=0) + + # Log for debugging + if ( + self.rank == 0 + and logger.isEnabledFor(logging.DEBUG) + and frame_idx is not None + ): + suppress_i_mask = suppress_i_mask.cpu().numpy() + suppress_j_mask = suppress_j_mask.cpu().numpy() + last_occluded = last_occluded.cpu().numpy() + + # Find all suppression pairs without using torch.where + batch_size = suppress_i_mask.shape[0] + + # Log i-suppression cases (where i gets suppressed in favor of j) + for i in range(batch_size): + for j in range(batch_size): + if suppress_i_mask[i, j]: + logger.debug( + f"{frame_idx=}: Suppressing obj {obj_ids[i]} last occluded {last_occluded[i]} in favor of {obj_ids[j]} last occluded {last_occluded[j]}" + ) + + # Log j-suppression cases (where j gets suppressed in favor of i) + for i in range(batch_size): + for j in range(batch_size): + if suppress_j_mask[i, j]: + logger.debug( + f"{frame_idx=}: Suppressing obj {obj_ids[j]} last occluded {last_occluded[j]} in favor of {obj_ids[i]} last occluded {last_occluded[i]}" + ) + + return to_suppress + + def _propogate_tracker_one_frame_local_gpu( + self, + inference_states: List[Any], + frame_idx: int, + reverse: bool, + # by default, we disable memory encoding until we gather all outputs + run_mem_encoder: bool = False, + # When specified, only return masks/scores for these object ids + filter_obj_ids: Optional[List[int]] = None, + ): + """ + inference_states: List of inference states, each state corresponds to a different set of objects. + """ + obj_ids_local = [] + low_res_masks_list = [] + obj_scores_list = [] + for inference_state in inference_states: + if len(inference_state["obj_ids"]) == 0: + continue # skip propagation on empty inference states + + # propagate one frame + num_frames_propagated = 0 + with torch.profiler.record_function("sam2_predictor.propagate_in_video"): + for out in self.tracker.propagate_in_video( + inference_state, + start_frame_idx=frame_idx, + # end_frame_idx = start_frame_idx + max_frame_num_to_track + # (i.e. propagating 1 frame since end_frame_idx is inclusive) + max_frame_num_to_track=0, + reverse=reverse, + tqdm_disable=True, + run_mem_encoder=run_mem_encoder, + ): + # TODO we only need low-res outputs here for all-gather across GPUs, + # so we can remove the high-res interpolation in `propagate_in_video` + out_frame_idx, out_obj_ids, out_low_res_masks, _, out_obj_scores = ( + out + ) + num_frames_propagated += 1 + + # only 1 frames should be propagated + assert ( + num_frames_propagated == 1 and out_frame_idx == frame_idx + ), f"num_frames_propagated: {num_frames_propagated}, out_frame_idx: {out_frame_idx}, frame_idx: {frame_idx}" + assert isinstance(out_obj_ids, list) + # Optionally filter to a subset of object ids (for partial propagation). + # We also clamp indices to available rows to avoid CUDA index_select assertions. + if filter_obj_ids is not None: + if len(out_obj_ids) > 0: + max_mask_rows = out_low_res_masks.shape[0] + max_score_rows = out_obj_scores.shape[0] + # Special case: common single-object refinement path where SAM2 returns a single mask row + # but a longer out_obj_ids list for the state. Treat the lone row as the requested object. + if ( + len(filter_obj_ids) == 1 + and max_mask_rows == 1 + and max_score_rows == 1 + ): + out_obj_ids = [filter_obj_ids[0]] + keep_indices = [0] + else: + keep_indices = [ + i + for i, oid in enumerate(out_obj_ids) + if oid in filter_obj_ids + and i < max_mask_rows + and i < max_score_rows + ] + else: + keep_indices = [] + if len(keep_indices) > 0: + idx_tensor = torch.as_tensor( + keep_indices, device=out_low_res_masks.device, dtype=torch.long + ) + out_low_res_masks = out_low_res_masks.index_select( + dim=0, index=idx_tensor + ) + out_obj_scores = out_obj_scores.index_select( + dim=0, index=idx_tensor + ) + out_obj_ids = [out_obj_ids[i] for i in keep_indices] + else: + # no selected objects in this local state; skip appending + out_obj_ids = [] + + if len(out_obj_ids) > 0: + obj_ids_local.extend(out_obj_ids) + low_res_masks_list.append(out_low_res_masks.squeeze(1)) + obj_scores_list.append(out_obj_scores.squeeze(1)) + + # concatenate the output masklets from all local inference states + + with torch.profiler.record_function( + "sam2_predictor.propagate_in_video.fill_holes" + ): + H_mask = W_mask = self.tracker.low_res_mask_size + if len(low_res_masks_list) > 0: + low_res_masks_local = torch.cat(low_res_masks_list, dim=0) + obj_scores_local = torch.cat(obj_scores_list, dim=0) + assert low_res_masks_local.shape[1:] == (H_mask, W_mask) + + # Apply hole filling to the masks + low_res_masks_local = fill_holes_in_mask_scores( + low_res_masks_local.unsqueeze(1), + fill_hole_area=self.fill_hole_area, + sprinkle_removal_area=self.sprinkle_removal_area, + fill_holes=True, + remove_sprinkles=True, + ) + low_res_masks_local = low_res_masks_local.squeeze(1) + else: + low_res_masks_local = torch.zeros(0, H_mask, W_mask, device=self.device) + obj_scores_local = torch.zeros(0, device=self.device) + + if self.is_multiplex and self.tracker.is_multiplex_dynamic: + # obj_ids_local might not be sorted, which is problematic because + # the rest of the code assumes that they are. + # Currently this only happens in the dynamic multiplex setting (since we backfill states) + # so we only check for this condition here, but this should be generally applicable. + # Note that a similar remapping is necessary when we update the memory, e.g., + # in _tracker_update_memories + if obj_ids_local != sorted(obj_ids_local): + # Get sorting permutation + sort_indices = sorted( + range(len(obj_ids_local)), key=lambda i: obj_ids_local[i] + ) + # Apply permutation to reorder everything + obj_ids_local = [obj_ids_local[i] for i in sort_indices] + low_res_masks_local = low_res_masks_local[sort_indices] + obj_scores_local = obj_scores_local[sort_indices] + + if self.is_multiplex and self.tracker.is_multiplex_dynamic: + # obj_ids_local might not be sorted, which is problematic because + # the rest of the code assumes that they are. + # Currently this only happens in the dynamic multiplex setting (since we backfill states) + # so we only check for this condition here, but this should be generally applicable. + # Note that a similar remapping is necessary when we update the memory, e.g., + # in _tracker_update_memories + if obj_ids_local != sorted(obj_ids_local): + # Get sorting permutation + sort_indices = sorted( + range(len(obj_ids_local)), key=lambda i: obj_ids_local[i] + ) + # Apply permutation to reorder everything + obj_ids_local = [obj_ids_local[i] for i in sort_indices] + if low_res_masks_local.shape[0] == len(sort_indices): + low_res_masks_local = low_res_masks_local[sort_indices] + obj_scores_local = obj_scores_local[sort_indices] + + return obj_ids_local, low_res_masks_local, obj_scores_local + + def _associate_det_trk( + self, + det_masks: Tensor, + det_scores: Tensor, + det_keep: Tensor, + trk_masks: Tensor, + trk_obj_ids: np.ndarray, + default_det_thresh: Optional[float] = None, + ): + """ + Match detections on the current frame with the existing masklets. + + Args: + - det_masks: (N, H, W) tensor of predicted masks + - det_scores: (N,) array of detection scores + - trk_masks: (M, H, W) tensor of track masks + - trk_obj_ids: (M,) array of object IDs corresponding to trk_masks + + Returns: + - new_det_fa_inds: array of new object indices among in FA detection outputs + - unmatched_trk_obj_ids: array of existing masklet object IDs that are not matched + to any detections on this frame (for unmatched, we only count masklets with >0 area) + - det_to_matched_trk_obj_ids: dict[int, np.ndarray]: mapping from FA detection indices + to the list of matched tracklet object IDs + - empty_trk_obj_ids: array of existing masklet object IDs with zero area in SAM2 prediction + """ + HIGH_CONF_THRESH = 0.8 + + iou_threshold = self.assoc_iou_thresh + iou_threshold_trk = self.trk_assoc_iou_thresh + new_det_thresh = ( + self.new_det_thresh if default_det_thresh is None else default_det_thresh + ) + + assert det_masks.is_floating_point(), "float tensor expected (do not binarize)" + assert trk_masks.is_floating_point(), "float tensor expected (do not binarize)" + assert ( + trk_masks.size(0) == len(trk_obj_ids) + ), f"trk_masks and trk_obj_ids should have the same length, {trk_masks.size(0)} vs {len(trk_obj_ids)}" + if trk_masks.size(0) == 0: + with torch.profiler.record_function("No tracklets"): + num_trk = 0 + is_new_det = det_scores >= new_det_thresh + trk_is_unmatched = torch.zeros( + num_trk, dtype=torch.bool, device=det_scores.device + ) + trk_is_nonempty = torch.zeros( + num_trk, dtype=torch.bool, device=det_scores.device + ) + num_det = det_scores.shape[0] + det_to_max_iou_trk_idx = torch.full( + (num_det,), -1, dtype=torch.long, device=det_scores.device + ) + det_is_high_conf = det_scores >= HIGH_CONF_THRESH + det_is_high_iou = torch.zeros( + num_det, dtype=torch.bool, device=det_scores.device + ) + im_mask = torch.zeros( + num_det, num_trk, dtype=torch.bool, device=det_scores.device + ) + return LazyAssociateDetTrkResult( + trk_is_unmatched, + trk_is_nonempty, + is_new_det, + det_to_max_iou_trk_idx, + det_is_high_conf, + det_is_high_iou, + det_keep, + im_mask, + ) + elif det_masks.size(0) == 0: + with torch.profiler.record_function("No detections"): + assert det_keep.size(0) == 0 # Make sure the keep mask agrees + trk_is_nonempty = (trk_masks > 0).any(dim=(1, 2)) + num_det = 0 + num_trk = trk_masks.shape[0] + trk_is_unmatched = torch.ones( + num_trk, dtype=torch.bool, device=trk_masks.device + ) + trk_is_nonempty_tensor = trk_is_nonempty.to(trk_masks.device) + is_new_det = torch.zeros( + num_det, dtype=torch.bool, device=trk_masks.device + ) + det_to_max_iou_trk_idx = torch.full( + (num_det,), -1, dtype=torch.long, device=trk_masks.device + ) + det_is_high_conf = torch.zeros( + num_det, dtype=torch.bool, device=trk_masks.device + ) + det_is_high_iou = torch.zeros( + num_det, dtype=torch.bool, device=trk_masks.device + ) + im_mask = torch.zeros( + num_det, num_trk, dtype=torch.bool, device=trk_masks.device + ) + return LazyAssociateDetTrkResult( + trk_is_unmatched, + trk_is_nonempty_tensor, + is_new_det, + det_to_max_iou_trk_idx, + det_is_high_conf, + det_is_high_iou, + det_keep, + im_mask, + ) + + if det_masks.shape[-2:] != trk_masks.shape[-2:]: + # resize to the smaller size to save GPU memory + if np.prod(det_masks.shape[-2:]) < np.prod(trk_masks.shape[-2:]): + trk_masks = F.interpolate( + trk_masks.unsqueeze(1), + size=det_masks.shape[-2:], + mode="bilinear", + align_corners=False, + ).squeeze(1) + else: + # resize detections to track size + det_masks = F.interpolate( + det_masks.unsqueeze(1), + size=trk_masks.shape[-2:], + mode="bilinear", + align_corners=False, + ).squeeze(1) + + with torch.profiler.record_function("associate_det_trk_compilable"): + if trk_masks.shape[0] < self.max_num_objects: + padding_size = self.max_num_objects - trk_masks.shape[0] + trk_masks_padded = torch.cat( + [ + trk_masks, + torch.zeros( + padding_size, + *trk_masks.shape[1:], + device=trk_masks.device, + dtype=trk_masks.dtype, + ), + ], + dim=0, + ) + else: + trk_masks_padded = trk_masks + result = _associate_det_trk_compilable( + det_masks, + det_scores, + det_keep, + trk_masks_padded, + new_det_thresh, + iou_threshold_trk, + iou_threshold, + HIGH_CONF_THRESH, + self.use_iom_recondition, + self.o2o_matching_masklets_enable, + self.iom_thresh_recondition, + self.iou_thresh_recondition, + ) + ( + trk_is_unmatched, + trk_is_nonempty, + is_new_det, + det_to_max_iou_trk_idx, + det_is_high_conf, + det_is_high_iou, + det_keep, + im_mask, + ) = result + trk_is_unmatched = trk_is_unmatched[: trk_masks.shape[0]] + trk_is_nonempty = trk_is_nonempty[: trk_masks.shape[0]] + im_mask = im_mask[:, : trk_masks.shape[0]] + + return LazyAssociateDetTrkResult( + trk_is_unmatched, + trk_is_nonempty, + is_new_det, + det_to_max_iou_trk_idx, + det_is_high_conf, + det_is_high_iou, + det_keep, + im_mask, + ) + + def _assign_new_det_to_gpus(self, new_det_num, prev_workload_per_gpu): + """Distribute the new objects to the GPUs with the least workload.""" + workload_per_gpu: np.ndarray = prev_workload_per_gpu.copy() + new_det_gpu_ids = np.zeros(new_det_num, np.int64) + + if self.is_multiplex: + # assign the objects in a batch of multiplex_count + for i in range(0, new_det_num, self.bucket_capacity): + # find the GPU with the least workload + min_gpu = np.argmin(workload_per_gpu) + new_det_gpu_ids[i : i + self.bucket_capacity] = min_gpu + workload_per_gpu[min_gpu] += 1 + else: + # assign the objects one by one + for i in range(len(new_det_gpu_ids)): + # find the GPU with the least workload + min_gpu = np.argmin(workload_per_gpu) + new_det_gpu_ids[i] = min_gpu + workload_per_gpu[min_gpu] += 1 + return new_det_gpu_ids + + def _process_hotstart_gpu( + self, + frame_idx: int, + reverse: bool, + adt_result, # LazyAssociateDetTrkResult (always lazy now) + tracker_metadata_prev: Dict[str, Any], + gpu_metadata_prev: Dict[str, Tensor], + ) -> Tuple[Tensor, Tensor, Dict[str, Tensor]]: + """ + Compute removal/suppression masks entirely on GPU without ANY syncs or branches. + + Uses position-indexed metadata (indexed 0 to N_obj-1) instead of obj_id-indexed + to avoid needing obj_ids as GPU tensor. + + Returns: + to_remove: boolean tensor (N_obj,) - objects to remove this frame + to_suppress: boolean tensor (N_obj,) - objec ts to suppress (overlap suppression) + gpu_metadata_new: updated GPU metadata for next frame + """ + # Handle edge case: if adt_result is already realized (no tracks exist), + # return empty masks since there's nothing to remove + if isinstance(adt_result, RealizedAssociateDetTrkresult): + # No tracks exist, so no objects to remove/suppress + empty_mask = torch.zeros(0, dtype=torch.bool, device=self.device) + return empty_mask, empty_mask, {"N_obj": 0} + + device = adt_result.trk_is_unmatched.device + N_obj = adt_result.trk_is_unmatched.size(0) # Number of current objects + + # ============================================================================ + # STEP 1: Initialize/extract position-indexed GPU metadata + # ============================================================================ + + # All metadata tensors are indexed by POSITION (0 to N_obj-1), not by obj_id + # This grows/shrinks each frame as objects are added/removed + + # Get previous frame's metadata (sized for previous N_obj) + # NOTE: Metadata is already compacted from previous frame (removed objects are already filtered out) + prev_N_obj = gpu_metadata_prev.get("N_obj", 0) + + if prev_N_obj > 0: + # Metadata from previous frame (position-indexed, already compacted) + obj_first_frame_prev = gpu_metadata_prev["obj_first_frame"] # (prev_N_obj,) + consecutive_unmatch_count_prev = gpu_metadata_prev[ + "consecutive_unmatch_count" + ] # (prev_N_obj,) + trk_keep_alive_prev = gpu_metadata_prev["trk_keep_alive"] # (prev_N_obj,) + removed_mask_prev = gpu_metadata_prev[ + "removed_mask" + ] # (prev_N_obj,) - should be all False after compaction + overlap_pair_counts_prev = gpu_metadata_prev[ + "overlap_pair_counts" + ] # (prev_N_obj, prev_N_obj) + last_occluded_prev = gpu_metadata_prev[ + "last_occluded_tensor" + ] # (prev_N_obj,) + else: + # First frame - no previous metadata + obj_first_frame_prev = None + consecutive_unmatch_count_prev = None + trk_keep_alive_prev = None + removed_mask_prev = None + overlap_pair_counts_prev = None + last_occluded_prev = None + + # ============================================================================ + # STEP 2: Carry forward metadata from previous frame + # ============================================================================ + + # Current frame has N_obj objects (from propagation) + # New objects are added via extend_gpu_metadata_for_new_objects AFTER compaction, + # so prev_N_obj should already include objects detected on previous frame. + # N_obj should equal prev_N_obj (no new objects mid-planning-phase). + assert ( + N_obj == prev_N_obj + ), f"N_obj ({N_obj}) should equal prev_N_obj ({prev_N_obj}); new objects handled after compaction" + + # Carry forward existing metadata (or initialize if first frame) + NEVER_OCCLUDED = -1 + obj_first_frame = ( + obj_first_frame_prev + if obj_first_frame_prev is not None + else torch.full((N_obj,), frame_idx, dtype=torch.long, device=device) + ) + consecutive_unmatch_count = ( + consecutive_unmatch_count_prev + if consecutive_unmatch_count_prev is not None + else torch.zeros(N_obj, dtype=torch.long, device=device) + ) + trk_keep_alive = ( + trk_keep_alive_prev + if trk_keep_alive_prev is not None + else torch.zeros(N_obj, dtype=torch.long, device=device) + ) + removed_mask = ( + removed_mask_prev + if removed_mask_prev is not None + else torch.zeros(N_obj, dtype=torch.bool, device=device) + ) + overlap_pair_counts = ( + overlap_pair_counts_prev + if overlap_pair_counts_prev is not None + else torch.zeros((N_obj, N_obj), dtype=torch.long, device=device) + ) + last_occluded = ( + last_occluded_prev + if last_occluded_prev is not None + else torch.full((N_obj,), NEVER_OCCLUDED, dtype=torch.long, device=device) + ) + + # ============================================================================ + # STEP 3: Update keep-alive counters (fully vectorized) + # ============================================================================ + + # Determine which tracks are matched by ANY detection + trk_is_matched = adt_result.im_mask.any(dim=0) # (N_obj,) + + # Update: +1 for matched, -1 for unmatched, clamp to [min, max] + trk_keep_alive = torch.where( + trk_is_matched, trk_keep_alive + 1, trk_keep_alive - 1 + ) + trk_keep_alive = torch.clamp( + trk_keep_alive, min=self.min_trk_keep_alive, max=self.max_trk_keep_alive + ) + + # Also decrement for empty tracklets (if configured) + if self.decrease_trk_keep_alive_for_empty_masklets: + trk_keep_alive = torch.where( + ~adt_result.trk_is_nonempty, + torch.clamp(trk_keep_alive - 1, min=self.min_trk_keep_alive), + trk_keep_alive, + ) + + # ============================================================================ + # STEP 4: Update total unmatch counters (fully vectorized) + # ============================================================================ + + # Increment for unmatched, but DON'T reset for matched + # Original logic accumulates total unmatched frames, not consecutive + consecutive_unmatch_count = torch.where( + adt_result.trk_is_unmatched, + consecutive_unmatch_count + 1, + consecutive_unmatch_count, # Keep previous value, don't reset + ) + + # ============================================================================ + # STEP 5: Update pairwise overlap tracking (fully vectorized) + # ============================================================================ + + # Find detections matched by multiple tracks + tracks_per_det = adt_result.im_mask.sum(dim=1) # (N_det,) + multi_match_mask = tracks_per_det > 1 # (N_det,) + + # Build overlap increment matrix using einsum + multi_match_tracks = adt_result.im_mask & multi_match_mask.unsqueeze( + 1 + ) # (N_det, N_obj) + + # Compute pairwise overlaps: for each detection, outer product of matched tracks + pairwise_overlap_this_frame = torch.einsum( + "di,dj->dij", multi_match_tracks.float(), multi_match_tracks.float() + ) # (N_det, N_obj, N_obj) + + # Sum across detections + overlap_increment = pairwise_overlap_this_frame.sum(dim=0) # (N_obj, N_obj) + overlap_increment.fill_diagonal_(0) # No self-overlap + overlap_increment = torch.triu( + overlap_increment, diagonal=1 + ) # Upper triangle only + + # Add this frame's increments (accumulate across frames, don't reset) + # Original logic: overlap_pair_to_frame_inds[key].append(frame_idx) - never clears + overlap_pair_counts = overlap_pair_counts + overlap_increment.long() + + # ============================================================================ + # STEP 6: Compute removal decisions - UNMATCH criterion (fully vectorized) + # ============================================================================ + + # Hotstart boundary + hotstart_diff = ( + frame_idx - self.hotstart_delay + if not reverse + else frame_idx + self.hotstart_delay + ) + + # Check if objects are within hotstart window + is_within_hotstart = ( + (obj_first_frame > hotstart_diff) + if not reverse + else (obj_first_frame < hotstart_diff) + ) + + # Remove if: within hotstart AND unmatched >= threshold AND not already removed + remove_by_unmatch = ( + is_within_hotstart + & (consecutive_unmatch_count >= self.hotstart_unmatch_thresh) + & ~removed_mask + ) + + # Suppress if: keep_alive <= 0 AND not hotstart-only mode AND not removed + suppress_by_unmatch = ( + (trk_keep_alive <= 0) + & torch.tensor(not self.suppress_unmatched_only_within_hotstart) + .pin_memory() + .to(device=device, non_blocking=True) + & ~removed_mask + & ~remove_by_unmatch + ) + + # ============================================================================ + # STEP 7: Compute removal decisions - OVERLAP criterion (fully vectorized) + # ============================================================================ + + # For each object, find max overlap count with any EARLIER object + # "Earlier" = appeared in an earlier frame + + # Build matrix: is_earlier[i, j] = True if object i appeared before object j + first_frames_i = obj_first_frame.unsqueeze(1) # (N_obj, 1) + first_frames_j = obj_first_frame.unsqueeze(0) # (1, N_obj) + + if not reverse: + is_earlier_matrix = first_frames_i < first_frames_j # (N_obj, N_obj) + else: + is_earlier_matrix = first_frames_i > first_frames_j # (N_obj, N_obj) + + # ============================================================================ + # STEP 8: Combine removal/suppression decisions + # ============================================================================ + + # Mask overlap counts to only consider earlier objects + if N_obj == 0: + to_remove = remove_by_unmatch + else: + overlap_with_earlier = torch.where( + is_earlier_matrix, + overlap_pair_counts, + torch.zeros_like(overlap_pair_counts), + ) + + # For each object (column j), find max overlap with any earlier object (row i) + max_overlap_with_earlier, _ = overlap_with_earlier.max(dim=0) # (N_obj,) + + # Remove if: within hotstart AND overlapped with earlier >= threshold + remove_by_overlap = ( + is_within_hotstart + & (max_overlap_with_earlier >= self.hotstart_dup_thresh) + & ~removed_mask + ) + + to_remove = remove_by_unmatch | remove_by_overlap # (N_obj,) + + to_suppress = suppress_by_unmatch # (N_obj,) + + # Update removed mask for future frames + removed_mask = removed_mask | to_remove + + # ============================================================================ + # STEP 9: Package updated metadata (NO SYNCS) + # ============================================================================ + + gpu_metadata_new = { + "N_obj": N_obj, + "obj_first_frame": obj_first_frame, + "consecutive_unmatch_count": consecutive_unmatch_count, + "trk_keep_alive": trk_keep_alive, + "removed_mask": removed_mask, + "overlap_pair_counts": overlap_pair_counts, + "last_occluded_tensor": last_occluded, + } + + return to_remove, to_suppress, gpu_metadata_new + + def _process_hotstart( + self, + frame_idx: int, + num_frames: int, + reverse: bool, + det_to_matched_trk_obj_ids: Dict[int, np.ndarray], + new_det_obj_ids: np.ndarray, + empty_trk_obj_ids: np.ndarray, + unmatched_trk_obj_ids: np.ndarray, + rank0_metadata: Dict[str, Any], + tracker_metadata: Dict[str, Any], + ): + """Handle hotstart heuristics to remove unmatched or duplicated objects.""" + # obj_id --> first frame index where the object was detected + obj_first_frame_idx = rank0_metadata["obj_first_frame_idx"] + # obj_id --> [mismatched frame indices] + unmatched_frame_inds = rank0_metadata["unmatched_frame_inds"] + trk_keep_alive = rank0_metadata["trk_keep_alive"] + # (first_appear_obj_id, obj_id) --> [overlap frame indices] + overlap_pair_to_frame_inds = rank0_metadata["overlap_pair_to_frame_inds"] + # removed_obj_ids: object IDs that are suppressed via hot-start + removed_obj_ids = rank0_metadata["removed_obj_ids"] + suppressed_obj_ids = rank0_metadata["suppressed_obj_ids"][frame_idx] + + obj_ids_newly_removed = set() # object IDs to be newly removed on this frame + hotstart_diff = ( + frame_idx - self.hotstart_delay + if not reverse + else frame_idx + self.hotstart_delay + ) + + # Step 1: log the frame index where each object ID first appears + for obj_id in new_det_obj_ids: + if obj_id not in obj_first_frame_idx: + obj_first_frame_idx[obj_id] = frame_idx + assert obj_id not in trk_keep_alive + trk_keep_alive[obj_id] = self.init_trk_keep_alive + + matched_trks = set() + # We use the det-->tracks list to check for matched objects. Otherwise, we need to compute areas to decide whether they're occluded + for matched_trks_per_det in det_to_matched_trk_obj_ids.values(): + matched_trks.update(matched_trks_per_det) + for obj_id in matched_trks: + # NOTE: To minimize number of configurable params, we use the hotstart_unmatch_thresh to set the max value of trk_keep_alive + trk_keep_alive[obj_id] = min( + self.max_trk_keep_alive, trk_keep_alive[obj_id] + 1 + ) + for obj_id in unmatched_trk_obj_ids: + unmatched_frame_inds[obj_id].append(frame_idx) + # NOTE: To minimize number of configurable params, we use the hotstart_unmatch_thresh to set the min value of trk_keep_alive + # The max keep alive is 2x the min, means the model prefers to keep the prediction rather than suppress it if it was matched long enough. + trk_keep_alive[obj_id] = max( + self.min_trk_keep_alive, trk_keep_alive[obj_id] - 1 + ) + if self.decrease_trk_keep_alive_for_empty_masklets: + for obj_id in empty_trk_obj_ids: + # NOTE: To minimize number of configurable params, we use the hotstart_unmatch_thresh to set the min value of trk_keep_alive + trk_keep_alive[obj_id] = max( + self.min_trk_keep_alive, trk_keep_alive[obj_id] - 1 + ) + + # Step 2: removed tracks that has not matched with detections for `hotstart_unmatch_thresh` frames with hotstart period + # a) add unmatched frame indices for each existing object ID + # note that `unmatched_trk_obj_ids` contains those frames where the SAM2 output mask + # doesn't match any FA detection; it excludes those frames where SAM2 gives an empty mask + # b) remove a masklet if it first appears after `hotstart_diff` and is unmatched for more + # than `self.hotstart_unmatch_thresh` frames + for obj_id, frame_indices in unmatched_frame_inds.items(): + if obj_id in removed_obj_ids or obj_id in obj_ids_newly_removed: + continue # skip if the object is already removed + if len(frame_indices) >= self.hotstart_unmatch_thresh: + is_within_hotstart = ( + obj_first_frame_idx[obj_id] > hotstart_diff and not reverse + ) or (obj_first_frame_idx[obj_id] < hotstart_diff and reverse) + if is_within_hotstart: + obj_ids_newly_removed.add(obj_id) + logger.info( + f"Removing object {obj_id} at frame {frame_idx} " + f"since it is unmatched for frames: {frame_indices}" + ) + if ( + trk_keep_alive[obj_id] <= 0 # Object has not been matched for too long + and not self.suppress_unmatched_only_within_hotstart + and obj_id not in removed_obj_ids + and obj_id not in obj_ids_newly_removed + ): + logger.debug( + f"Suppressing object {obj_id} at frame {frame_idx}, due to being unmatched" + ) + suppressed_obj_ids.add(obj_id) + + # Step 3: removed tracks that overlaps with another track for `hotstart_dup_thresh` frames + # a) find overlaps tracks -- we consider overlap if they match to the same detection + for _, matched_trk_obj_ids in det_to_matched_trk_obj_ids.items(): + if len(matched_trk_obj_ids) < 2: + continue # only count detections that are matched to multiple (>=2) masklets + # if there are multiple matched track ids, we need to find the one that appeared first; + # these later appearing ids may be removed since they may be considered as duplicates + first_appear_obj_id = ( + min(matched_trk_obj_ids, key=lambda x: obj_first_frame_idx[x]) + if not reverse + else max(matched_trk_obj_ids, key=lambda x: obj_first_frame_idx[x]) + ) + for obj_id in matched_trk_obj_ids: + if obj_id != first_appear_obj_id: + key = (first_appear_obj_id, obj_id) + overlap_pair_to_frame_inds[key].append(frame_idx) + + # b) remove a masklet if it first appears after `hotstart_diff` and it overlaps with another + # masklet (that appears earlier) for more than `self.hotstart_dup_thresh` frames + for (first_obj_id, obj_id), frame_indices in overlap_pair_to_frame_inds.items(): + if obj_id in removed_obj_ids or obj_id in obj_ids_newly_removed: + continue # skip if the object is already removed + if (obj_first_frame_idx[obj_id] > hotstart_diff and not reverse) or ( + obj_first_frame_idx[obj_id] < hotstart_diff and reverse + ): + if len(frame_indices) >= self.hotstart_dup_thresh: + obj_ids_newly_removed.add(obj_id) + logger.info( + f"Removing object {obj_id} at frame {frame_idx} " + f"since it overlaps with another track {first_obj_id} at frames: {frame_indices}" + ) + + removed_obj_ids.update(obj_ids_newly_removed) + return obj_ids_newly_removed, rank0_metadata + + def _tracker_update_memories( + self, + sam2_inference_states: List[Any], + frame_idx: int, + tracker_metadata: Dict[str, Any], + low_res_masks: Tensor, + ): + """ + Run Sam2 memory encoder, enforcing non-overlapping constraints globally. + """ + # TODO: Add most recently occluded heuristic for suppression of overlapping masks + if len(sam2_inference_states) == 0: + return + # Avoid an extra interpolation step by directly interpolating to `interpol_size` + high_res_H, high_res_W = ( + self.tracker.maskmem_backbone.mask_downsampler.interpol_size + ) + # NOTE: inspect this part if we observe OOMs in the demo + high_res_masks = F.interpolate( + low_res_masks.unsqueeze(1), + size=(high_res_H, high_res_W), + mode="bilinear", + align_corners=False, + ) + # We first apply non-overlapping constraints before memory encoding. This may include some suppression heuristics. + with torch.profiler.record_function( + "sam2_predictor.propagate_in_video.apply_non_overlapping_constraints" + ): + # TODO: try _apply_object_wise_non_overlapping_constraints instead + high_res_masks = self.tracker._suppress_object_pw_area_shrinkage( + high_res_masks + ) + # Instead of gathering the predicted object scores, we use mask areas as a proxy. + object_score_logits = torch.where( + (high_res_masks > 0).any(dim=(-1, -2)), 10.0, -10.0 + ) + + if self.is_multiplex and self.tracker.is_multiplex_dynamic: + # The objects in the masks are ordered w.r.t. object IDs, + # which might not be true in the dynamic multiplex case with backfilling + # (see also _propogate_tracker_one_frame_local_gpu) + # We need to plan globally for the mask assignment here + object_idx_assignment: dict[int, list[int]] = {} + all_object_ids: list[int] = [] + object_id_to_state_i: dict[int, int] = {} + for state_i, sam2_state in enumerate(sam2_inference_states): + obj_ids = sam2_state["obj_ids"] + all_object_ids.extend(obj_ids) + for obj_id in obj_ids: + object_id_to_state_i[obj_id] = state_i + object_idx_assignment[state_i] = [] + sorted_indices = sorted( + range(len(all_object_ids)), key=lambda i: all_object_ids[i] + ) + # Build the object_idx_assignment mapping + for global_idx, local_idx in enumerate(sorted_indices): + obj_id = all_object_ids[local_idx] + object_idx_assignment[object_id_to_state_i[obj_id]].append(global_idx) + + # Run the memory encoder on local slices for each GPU + start_idx_gpu = sum(tracker_metadata["num_obj_per_gpu"][: self.rank]) + start_idx_state = start_idx_gpu + for state_i, sam2_state in enumerate(sam2_inference_states): + num_obj_per_state = len(sam2_state["obj_ids"]) + if num_obj_per_state == 0: + continue + # Get the local high-res masks and object score logits for this inference state + if self.is_multiplex and self.tracker.is_multiplex_dynamic: + local_idx = ( + torch.tensor(object_idx_assignment[state_i]) + .pin_memory() + .to(device=high_res_masks.device, non_blocking=True) + ) + local_high_res_masks = high_res_masks[local_idx] + local_object_score_logits = object_score_logits[local_idx] + else: + end_idx_state = start_idx_state + num_obj_per_state + local_high_res_masks = high_res_masks[start_idx_state:end_idx_state] + local_object_score_logits = object_score_logits[ + start_idx_state:end_idx_state + ] + local_batch_size = local_high_res_masks.size(0) + # Run Sam2 memory encoder. Note that we do not re-enforce the non-overlapping constraint as it is turned off by default + + encoded_mem = self.tracker._run_memory_encoder( + sam2_state, + frame_idx, + local_batch_size, + local_high_res_masks, + local_object_score_logits, + is_mask_from_pts=False, + ) + if self.is_multiplex: + ( + local_maskmem_features, + local_maskmem_pos_enc, + local_image_features, + local_image_pos_enc, + ) = encoded_mem + else: + local_maskmem_features, local_maskmem_pos_enc = encoded_mem + + # Store encoded memories in the local inference state + output_dict = sam2_state["output_dict"] + for storage_key in ["cond_frame_outputs", "non_cond_frame_outputs"]: + if frame_idx not in output_dict[storage_key]: + continue + output_dict[storage_key][frame_idx]["maskmem_features"] = ( + local_maskmem_features + ) + output_dict[storage_key][frame_idx]["maskmem_pos_enc"] = [ + pos for pos in local_maskmem_pos_enc + ] + if self.is_multiplex: + output_dict[storage_key][frame_idx]["image_features"] = ( + local_image_features + ) + output_dict[storage_key][frame_idx]["image_pos_enc"] = ( + local_image_pos_enc + ) + + if self.reapply_no_object_pointer: + # reapply the no_object_pointer projection for the objects suppressed by the heuristics + newly_suppressed_objects = ( + output_dict[storage_key][frame_idx]["object_score_logits"] + > self.tracker.object_score_logit_threshold + ) & (local_object_score_logits < 0) + if torch.any(newly_suppressed_objects): + existing_pointers = output_dict[storage_key][frame_idx][ + "obj_ptr" + ] + + multiplex_state = sam2_state["multiplex_state"] + existing_pointers = multiplex_state.demux(existing_pointers) + + newly_suppressed_objects = newly_suppressed_objects.float() + new_pointers = ( + newly_suppressed_objects + * self.tracker.no_obj_ptr_linear(existing_pointers) + + (1 - newly_suppressed_objects) * existing_pointers + ) + + output_dict[storage_key][frame_idx]["obj_ptr"] = ( + multiplex_state.mux(new_pointers) + ) + elif self.reapply_no_object_pointer: + raise NotImplementedError( + "reapply_no_object_pointer is not implemented for non-multiplex" + ) + + # for batched inference state, we also need to add per-object + # memory slides to support instance interactivity + self.tracker.add_output_per_object( + inference_state=sam2_state, + frame_idx=frame_idx, + current_out=output_dict[storage_key][frame_idx], + storage_key=storage_key, + ) + start_idx_state += num_obj_per_state + + def _tracker_add_new_objects( + self, + frame_idx: int, + num_frames: int, + new_obj_ids: List[int], + new_obj_masks: Tensor, + tracker_states_local: List[Any], + orig_vid_height: int, + orig_vid_width: int, + feature_cache: Dict, + ): + """Add new objects to SAM2 inference states.""" + + prev_sam2_state = ( + tracker_states_local[0] if len(tracker_states_local) > 0 else None + ) + # prepare inference_state + if self.tracker.is_multiplex_dynamic: + # in multiplex_dynamic mode, we first try to find the best-fit + # inference state for the new objects. + # Create a new state if needed + num_new_objects = len(new_obj_ids) + + # Try to find existing states with available slots + best_state = None + best_available_slots = float("inf") + + for state in tracker_states_local: + available_slots = state["multiplex_state"].available_slots + # Find the state with the least available slots that can still fit the new objects + if ( + available_slots >= num_new_objects + and available_slots < best_available_slots + ): + best_state = state + best_available_slots = available_slots + + if best_state is not None: + # Use the existing state with sufficient available slots + new_sam2_state = best_state + else: + # Need to create a new state + new_sam2_state = self.tracker.init_state( + cached_features=feature_cache, + video_height=orig_vid_height, + video_width=orig_vid_width, + num_frames=num_frames, + ) + new_sam2_state["backbone_out"] = ( + prev_sam2_state.get("backbone_out", None) + if prev_sam2_state is not None + else None + ) + # Add the new state to our local states list + tracker_states_local.append(new_sam2_state) + else: + if self.tracker.per_obj_inference: + # in per_obj_inference mode, init_state happens only once, + # new obj_ids will be added to the existing inference state + if prev_sam2_state is not None: + new_sam2_state = prev_sam2_state + else: + new_sam2_state = self.tracker.init_state( + cached_features=feature_cache, + video_height=orig_vid_height, + video_width=orig_vid_width, + num_frames=num_frames, + ) + new_sam2_state["backbone_out"] = None + tracker_states_local = [new_sam2_state] + else: + # batch objects that first appear on the same frame together + # Clear inference state. Keep the cached image features if available. + new_sam2_state = self.tracker.init_state( + cached_features=feature_cache, + video_height=orig_vid_height, + video_width=orig_vid_width, + num_frames=num_frames, + ) + new_sam2_state["backbone_out"] = ( + prev_sam2_state.get("backbone_out", None) + if prev_sam2_state is not None + else None + ) + tracker_states_local.append(new_sam2_state) + + assert len(new_obj_ids) == new_obj_masks.size(0) + assert new_obj_masks.is_floating_point() + # TODO consider removing this interpolation -- it's probably no longer needed + # we should edit `self.tracker.add_new_mask` to directly take low-res input masks + input_mask_res = self.tracker.input_mask_size + new_obj_masks = F.interpolate( + new_obj_masks.unsqueeze(1), + size=(input_mask_res, input_mask_res), + mode="bilinear", + align_corners=False, + ).squeeze(1) + new_obj_masks = new_obj_masks > 0 + + if self.is_multiplex: + # add all objects at once + # NOTE: In the current implementation, add_new_masks also runs the memory encoder + # the non-overlapping constraint is enforced + self.tracker.add_new_masks( + inference_state=new_sam2_state, + frame_idx=frame_idx, + obj_ids=new_obj_ids, + masks=new_obj_masks, + add_mask_to_memory=True, + ) + else: + # add object one by one + for new_obj_id, new_mask in zip(new_obj_ids, new_obj_masks): + self.tracker.add_new_mask( + inference_state=new_sam2_state, + frame_idx=frame_idx, + obj_id=new_obj_id, + mask=new_mask, + add_mask_to_memory=True, + ) + # NOTE: we skip enforcing the non-overlapping constraint **globally** when adding new objects. + self.tracker.propagate_in_video_preflight(new_sam2_state, run_mem_encoder=True) + + return tracker_states_local + + def _tracker_remove_objects( + self, tracker_states_local: List[Any], obj_ids: list[int] + ): + """ + Remove an object from SAM2 inference states. This would remove the object from + all frames in the video. + """ + if self.is_multiplex: + tracker_states_local_before_removal = tracker_states_local.copy() + tracker_states_local.clear() + for sam2_inference_state in tracker_states_local_before_removal: + # we try to remove `obj_id` on every inference state with `strict=False` + # it will not do anything if an inference state doesn't contain `obj_id` + new_obj_ids, _ = self.tracker.remove_objects( + sam2_inference_state, obj_ids, strict=False, need_output=False + ) + # only keep an inference state if it's non-empty after object removal + if len(new_obj_ids) > 0: + tracker_states_local.append(sam2_inference_state) + else: + for obj_id in obj_ids: + self._tracker_remove_object(tracker_states_local, obj_id) + + def update_masklet_confirmation_status( + self, + rank0_metadata: Dict[str, Any], + obj_ids_all_gpu_prev: np.ndarray, + obj_ids_all_gpu_updated: np.ndarray, + det_to_matched_trk_obj_ids: Dict[int, np.ndarray], + new_det_obj_ids: np.ndarray, + ): + """ + Update masklet confirmation status. + """ + confirmation_data = rank0_metadata["masklet_confirmation"] + status_prev = confirmation_data["status"] + consecutive_det_num_prev = confirmation_data["consecutive_det_num"] + + N_prev = len(obj_ids_all_gpu_prev) + N_updated = len(obj_ids_all_gpu_updated) + + # a) Map previous confirmation data to updated positions + # For small arrays, simple dict lookup is fast + unconfirmed_val = MaskletConfirmationStatus.UNCONFIRMED.value + status = np.full(N_updated, unconfirmed_val, dtype=np.int64) + consecutive_det_num = np.zeros(N_updated, dtype=np.int64) + + if N_prev > 0 and N_updated > 0: + # Build mapping: obj_id -> new index + obj_id_to_new_idx = { + obj_id: idx for idx, obj_id in enumerate(obj_ids_all_gpu_updated) + } + + # Copy previous values for objects that still exist + for old_idx, obj_id in enumerate(obj_ids_all_gpu_prev): + new_idx = obj_id_to_new_idx.get(obj_id) + if new_idx is not None: + status[new_idx] = status_prev[old_idx] + consecutive_det_num[new_idx] = consecutive_det_num_prev[old_idx] + + # b) Update confirmation status based on current frame detections + # Build set of all matched object IDs + matched_obj_ids = set(new_det_obj_ids) + for matched_trk_ids in det_to_matched_trk_obj_ids.values(): + matched_obj_ids.update(matched_trk_ids) + + # Update consecutive detection count and status + for idx, obj_id in enumerate(obj_ids_all_gpu_updated): + if obj_id in matched_obj_ids: + consecutive_det_num[idx] += 1 + else: + consecutive_det_num[idx] = 0 + + # Update status to CONFIRMED where threshold is met + if ( + consecutive_det_num[idx] + >= self.masklet_confirmation_consecutive_det_thresh + ): + status[idx] = MaskletConfirmationStatus.CONFIRMED.value + + # Store updated arrays + confirmation_data["status"] = status + confirmation_data["consecutive_det_num"] = consecutive_det_num + return rank0_metadata + + +class Sam3MultiplexPredictorWrapper(Sam3MultiplexTrackerPredictor): + """ + Wraps a pre-built multiplex tracker model with the same interface as the + onevision Sam3MultiplexTrackerPredictor class. Inherits from Sam3MultiplexTrackerPredictor to pass + isinstance checks, but skips Sam3MultiplexTrackerPredictor.__init__ (which requires Hydra). + + Provides bf16 autocast, attribute proxying, and configuration flags + needed by Sam3MultiplexTracking. + + The onevision Sam3MultiplexTrackerPredictor builds the tracker from Hydra config and applies + extensive hydra_overrides. This version skips Hydra entirely — the caller + is responsible for building the tracker via model_builder.py with the + correct parameters. + + Key parameters that the onevision Sam3MultiplexTrackerPredictor sets via hydra_overrides + (documented here for reference — these must be set in model_builder.py): + - image_size=1008, backbone_stride=14 + - maskmem_backbone.mask_downsampler.interpol_size=[1152,1152] + - always_start_from_first_ann_frame=false + - non_overlap_masks_for_mem_enc=false, non_overlap_masks_for_output=false + - max_cond_frames_in_attn=4 + - offload_output_to_cpu_for_eval=false, trim_past_non_cond_mem_for_eval=false + - sam_mask_decoder_extra_args: dynamic_multimask_via_stability=true, etc. + - binarize_mask_from_pts_for_mem_enc=true (SAM2 tracker default) + - only_obj_ptrs_in_the_past_for_eval=true + - clear_non_cond_mem_around_input=true + - transformer.encoder.layer.self_attention.feat_sizes=[72,72] + - transformer.encoder.layer.cross_attention.feat_sizes=[72,72] + - fill_hole_area= + - use_fa3, use_rope_real on self_attention, cross_attention, + self_attention_rope, cross_attention_rope + - use_memory_selection + """ + + def __init__( + self, + model, + per_obj_inference=False, + fill_hole_area=0, + is_multiplex=True, + is_multiplex_dynamic=True, + ): + # Skip Sam3MultiplexTrackerPredictor.__init__ (requires Hydra) — call nn.Module.__init__ directly + nn.Module.__init__(self) + self.model = model + self.per_obj_inference = per_obj_inference + self.fill_hole_area = fill_hole_area + self.is_multiplex = is_multiplex + self.is_multiplex_dynamic = is_multiplex_dynamic + + # use bfloat16 inference for Flash Attention kernel + self.bf16_context = torch.autocast(device_type="cuda", dtype=torch.bfloat16) + self.bf16_context.__enter__() diff --git a/sam3/model/sam3_multiplex_detector.py b/sam3/model/sam3_multiplex_detector.py new file mode 100644 index 0000000..be06d20 --- /dev/null +++ b/sam3/model/sam3_multiplex_detector.py @@ -0,0 +1,943 @@ +import os + +import torch +from sam3.model.vl_combiner import SAM3VLBackbone + +try: + from sam3.model.vl_combiner import SAM3VLBackboneTri +except ImportError: + SAM3VLBackboneTri = None +from typing import Dict, List, Optional + +import numpy as np +from sam3.model.data_misc import BatchedDatapoint, FindStage +from sam3.model.geometry_encoders import Prompt +from sam3.model.model_misc import SAM3Output +from sam3.model.sam3_image import Sam3Image +from sam3.model.sam3_multiplex_detector_utils import nms_masks + + +class Sam3MultiplexImageBase(Sam3Image): + """A wrapper class to run Sam3Image on videos for per-frame detection (no tracking).""" + + def __init__( + self, + *args, + tracking_score_thresh: float = 0.0, + offload_outputs_to_cpu_for_eval: bool = False, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.tracking_score_thresh = tracking_score_thresh + self.offload_outputs_to_cpu_for_eval = offload_outputs_to_cpu_for_eval + self.trim_outputs_for_eval = True # dummy option -- it doesn't do anything + + def forward( + self, + input: BatchedDatapoint, + is_inference=False, # (a dummy parameter not used anymore) + ): + assert ( + not self.training + ), "Sam3MultiplexImageBase should only be used in eval mode." + + device = self.device + backbone_out = {"img_batch_all_stages": input.img_batch} + text_outputs = self.backbone.forward_text(input.find_text_batch, device=device) + backbone_out.update(text_outputs) + num_frames = len(input.find_inputs) + + previous_stages_out = SAM3Output( + iter_mode=SAM3Output.IterMode.LAST_STEP_PER_STAGE + ) + for frame_idx in range(num_frames): + find_input = input.find_inputs[frame_idx] + find_target = input.find_targets[frame_idx] + geometric_prompt = self._get_geo_prompt_from_find_input(find_input) + cur_out, _ = self.forward_video_grounding( + backbone_out=backbone_out, + find_input=find_input, + find_target=find_target, + geometric_prompt=geometric_prompt, + ) + # offload model outputs to CPU (to save GPU memory) for evaluation + if self.offload_outputs_to_cpu_for_eval: + cur_out = {k: v.cpu() for k, v in cur_out.items()} + + previous_stages_out.append([cur_out]) + + get_queries = None + return previous_stages_out, get_queries + + def forward_video_grounding( + self, + backbone_out, + find_input, + find_target, + geometric_prompt: Prompt, + **kwargs, + ): + # route this to the image grounding forward method + out = self.forward_grounding( + backbone_out=backbone_out, + find_input=find_input, + find_target=find_target, + geometric_prompt=geometric_prompt, + ) + # trim the output to only include the necessary keys + out = { + "pred_logits": out["pred_logits"], + "pred_boxes": out["pred_boxes"], + "pred_boxes_xyxy": out["pred_boxes_xyxy"], + "pred_masks": out["pred_masks"], + "pred_object_ids": self._get_dummy_object_ids(out["pred_logits"]), + } + return out, backbone_out + + def _get_dummy_object_ids(self, pred_logits): + """Generate dummy object IDs for the detected objects, based on their detection query indices.""" + # Assuming pred_logits has shape [batch_size, num_queries, num_classes] + B, Q, _ = pred_logits.shape + is_above_thresh = pred_logits.squeeze(2) > self.tracking_score_thresh + dummy_obj_ids = torch.arange(Q, device=self.device).expand(B, -1) + dummy_obj_ids = torch.where(is_above_thresh, dummy_obj_ids, -1) + return dummy_obj_ids + + def _trim_outputs(self, *args, **kwargs): + pass # not needed for image-on-video + + def _batch_find_inputs( + self, + find_inputs: List[FindStage], + chunk_start: int, + chunk_end: int, + ) -> FindStage: + """ + Batch multiple FindStage objects into a single batched FindStage. + + For each frame in the chunk, creates img_ids that point to the correct + frame index. When processing streaming video, the img_ids are the actual + frame indices (e.g., [0, 1, 2, ..., 15] for chunk 0-16), and the modulo + for circular buffer access is applied later in _get_img_feats. + + Args: + find_inputs: List of FindStage objects for all frames. + chunk_start: Start index of the chunk. + chunk_end: End index of the chunk (exclusive). + + Returns: + A single FindStage with batched tensors. + """ + chunk_find_inputs = [ + find_inputs[i % len(find_inputs)] for i in range(chunk_start, chunk_end) + ] + + # Generate img_ids based on chunk frame indices + # Each frame in the chunk gets its corresponding frame index + # The modulo for circular buffer access is handled in _get_img_feats + device = chunk_find_inputs[0].img_ids.device + dtype = chunk_find_inputs[0].img_ids.dtype + img_ids_list = [ + torch.tensor([i], device=device, dtype=dtype) + for i in range(chunk_start, chunk_end) + ] + batched_img_ids = torch.cat(img_ids_list, dim=0) + + # Generate img_ids_np to match + img_ids_np_list = [np.array([i]) for i in range(chunk_start, chunk_end)] + batched_img_ids_np = np.concatenate(img_ids_np_list, axis=0) + + # Concatenate text_ids + text_ids_list = [fi.text_ids for fi in chunk_find_inputs] + batched_text_ids = torch.cat(text_ids_list, dim=0) + + # Concatenate input_boxes + input_boxes_list = [fi.input_boxes for fi in chunk_find_inputs] + batched_input_boxes = ( + torch.cat(input_boxes_list, dim=0) + if input_boxes_list[0] is not None + else None + ) + + # Concatenate input_boxes_mask + input_boxes_mask_list = [fi.input_boxes_mask for fi in chunk_find_inputs] + batched_input_boxes_mask = ( + torch.cat(input_boxes_mask_list, dim=0) + if input_boxes_mask_list[0] is not None + else None + ) + + # Concatenate input_boxes_label + input_boxes_label_list = [fi.input_boxes_label for fi in chunk_find_inputs] + batched_input_boxes_label = ( + torch.cat(input_boxes_label_list, dim=0) + if input_boxes_label_list[0] is not None + else None + ) + + # Concatenate input_points + input_points_list = [fi.input_points for fi in chunk_find_inputs] + batched_input_points = ( + torch.cat(input_points_list, dim=0) + if input_points_list[0] is not None + else None + ) + + # Concatenate input_points_mask + input_points_mask_list = [fi.input_points_mask for fi in chunk_find_inputs] + batched_input_points_mask = ( + torch.cat(input_points_mask_list, dim=0) + if input_points_mask_list[0] is not None + else None + ) + + # Handle optional fields + input_boxes_before_embed_list = [ + fi.input_boxes_before_embed for fi in chunk_find_inputs + ] + batched_input_boxes_before_embed = ( + torch.cat(input_boxes_before_embed_list, dim=0) + if input_boxes_before_embed_list[0] is not None + else None + ) + + input_points_before_embed_list = [ + fi.input_points_before_embed for fi in chunk_find_inputs + ] + batched_input_points_before_embed = ( + torch.cat(input_points_before_embed_list, dim=0) + if input_points_before_embed_list[0] is not None + else None + ) + + # Create batched FindStage + batched_find_input = FindStage( + img_ids=batched_img_ids, + img_ids_np=batched_img_ids_np, + text_ids=batched_text_ids, + input_boxes=batched_input_boxes, + input_boxes_mask=batched_input_boxes_mask, + input_boxes_label=batched_input_boxes_label, + input_points=batched_input_points, + input_points_mask=batched_input_points_mask, + ptrs=None, # Not batching pointers for now + ptrs_seg=None, + object_ids=None, + input_boxes_before_embed=batched_input_boxes_before_embed, + input_points_before_embed=batched_input_points_before_embed, + ) + + return batched_find_input + + def _batch_geometric_prompts( + self, + geometric_prompts: List[Prompt], + chunk_start: int, + chunk_end: int, + ) -> Prompt: + """ + Batch multiple Prompt objects into a single batched Prompt. + + Args: + geometric_prompts: List of Prompt objects for all frames. + chunk_start: Start index of the chunk. + chunk_end: End index of the chunk (exclusive). + + Returns: + A single Prompt with batched tensors. + """ + chunk_prompts = [geometric_prompts[i] for i in range(chunk_start, chunk_end)] + return self._batch_geometric_prompts_from_list(chunk_prompts) + + def _batch_geometric_prompts_from_list( + self, + chunk_prompts: List[Prompt], + ) -> Prompt: + """ + Batch a list of Prompt objects into a single batched Prompt. + + Prompt uses seq-first, batch-second convention: + - box_embeddings: N_boxes x B x C_box - batch along dim 1 + - box_mask: B x N_boxes - batch along dim 0 + - box_labels: N_boxes x B - batch along dim 1 + - point_embeddings: N_points x B x C_point - batch along dim 1 + - point_mask: B x N_points - batch along dim 0 + - point_labels: N_points x B - batch along dim 1 + + Args: + chunk_prompts: List of Prompt objects to batch. + + Returns: + A single Prompt with batched tensors. + """ + + # Helper function to batch tensors along specified dimension + def batch_tensors(tensors, dim): + if tensors[0] is None: + return None + return torch.cat(tensors, dim=dim) + + # Batch box embeddings (N_boxes x B x C_box - batch along dim 1) + box_embeddings_list = [p.box_embeddings for p in chunk_prompts] + batched_box_embeddings = batch_tensors(box_embeddings_list, dim=1) + + # Batch box mask (B x N_boxes - batch along dim 0) + box_mask_list = [p.box_mask for p in chunk_prompts] + batched_box_mask = batch_tensors(box_mask_list, dim=0) + + # Batch box labels (N_boxes x B - batch along dim 1) + box_labels_list = [p.box_labels for p in chunk_prompts] + batched_box_labels = batch_tensors(box_labels_list, dim=1) + + # Batch point embeddings (N_points x B x C_point - batch along dim 1) + point_embeddings_list = [p.point_embeddings for p in chunk_prompts] + batched_point_embeddings = batch_tensors(point_embeddings_list, dim=1) + + # Batch point mask (B x N_points - batch along dim 0) + point_mask_list = [p.point_mask for p in chunk_prompts] + batched_point_mask = batch_tensors(point_mask_list, dim=0) + + # Batch point labels (N_points x B - batch along dim 1) + point_labels_list = [p.point_labels for p in chunk_prompts] + batched_point_labels = batch_tensors(point_labels_list, dim=1) + + # Create batched Prompt + batched_prompt = Prompt( + box_embeddings=batched_box_embeddings, + box_mask=batched_box_mask, + box_labels=batched_box_labels, + point_embeddings=batched_point_embeddings, + point_mask=batched_point_mask, + point_labels=batched_point_labels, + ) + + return batched_prompt + + +class Sam3MultiplexDetector(Sam3MultiplexImageBase): + def __init__( + self, + *args, + async_all_gather=True, + gather_backbone_out=None, + is_multiplex=False, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.rank = int(os.getenv("RANK", "0")) + self.world_size = int(os.getenv("WORLD_SIZE", "1")) + self.async_all_gather = async_all_gather + + # if gather_backbone is not set, default to gathering only for `SAM3VLBackbone` + if gather_backbone_out is None: + gather_backbone_out = isinstance(self.backbone, SAM3VLBackbone) or ( + SAM3VLBackboneTri is not None + and isinstance(self.backbone, SAM3VLBackboneTri) + ) + self.gather_backbone_out = gather_backbone_out + self.is_multiplex = is_multiplex + + def forward_video_grounding_multigpu( + self, + backbone_out, + find_inputs, + geometric_prompt: Prompt, + frame_idx, + num_frames, + # `multigpu_buffer` is a dict to cache FA outputs in a chunk between different calls + multigpu_buffer, + track_in_reverse=False, + # whether to also return the SAM2 backbone features (in addition to FA results) + return_sam2_backbone_feats=False, + # whether to perform NMS and suppress the scores of those detections removed by NMS + run_nms=False, + nms_prob_thresh=None, + nms_iou_thresh=None, + nms_use_iom=False, + # tracking bounds to respect max_frame_num_to_track + max_frame_num_to_track=None, + propagate_in_video_start_frame_idx=None, + # feature_cache for buffered backbone computation + feature_cache=None, + **kwargs, + ): + """ + Compute the FA detection outputs in a distributed manner, where all GPUs process + a chunk of frames (equal to the number of GPUs) at once and store them in cache. + """ + # Calculate valid frame range based on max_frame_num_to_track + # We prevent pre-fetching beyond the tracking window relative to current frame + if max_frame_num_to_track is not None: + if propagate_in_video_start_frame_idx is None: + propagate_in_video_start_frame_idx = 0 + if track_in_reverse: + # When going backwards, limit how far back we can go from current frame + valid_frame_start = max( + 0, + propagate_in_video_start_frame_idx - max_frame_num_to_track + 1, + ) + valid_frame_end = num_frames + else: + # When going forwards, limit how far ahead we can go from current frame + valid_frame_start = 0 + valid_frame_end = min( + num_frames, + propagate_in_video_start_frame_idx + max_frame_num_to_track, + ) + else: + # No tracking limit specified, use full video range + valid_frame_start = 0 + valid_frame_end = num_frames + + # Step 1: fetch the FA outputs in the current chunk from buffer + frame_idx_curr_b = frame_idx - frame_idx % self.world_size + frame_idx_curr_e = min(frame_idx_curr_b + self.world_size, num_frames) + + # Clamp the current chunk to the valid tracking range + frame_idx_curr_b = max(frame_idx_curr_b, valid_frame_start) + frame_idx_curr_e = min(frame_idx_curr_e, valid_frame_end) + # in case the current frame's FA results are not in the buffer yet, build the current chunk + # (this should only happen on the first chunk, since we are also building the next chunk below) + if frame_idx not in multigpu_buffer: + with torch.profiler.record_function("build_multigpu_buffer_next_chunk1"): + self._build_multigpu_buffer_next_chunk( + backbone_out=backbone_out, + find_inputs=find_inputs, + geometric_prompt=geometric_prompt, + frame_idx_begin=frame_idx_curr_b, + frame_idx_end=frame_idx_curr_e, + num_frames=num_frames, + multigpu_buffer=multigpu_buffer, + run_nms=run_nms, + nms_prob_thresh=nms_prob_thresh, + nms_iou_thresh=nms_iou_thresh, + nms_use_iom=nms_use_iom, + feature_cache=feature_cache, + ) + + # read out the current frame's results from `multigpu_buffer` + out = {} + for k, (v, handle) in multigpu_buffer[frame_idx].items(): + if self.is_multiplex: + if ( + k.startswith("interactive_backbone_") + or k.startswith("propagation_backbone_") + ) and not return_sam2_backbone_feats: + continue + else: + if k.startswith("sam2_backbone_") and not return_sam2_backbone_feats: + continue + if handle is not None: + handle.wait() # wait for async all-gather to finish + out[k] = v + + # Step 2: remove FA outputs of the previous chunk from cache to save GPU memory + if not track_in_reverse and frame_idx_curr_b - self.world_size >= 0: + frame_idx_prev_e = frame_idx_curr_b + frame_idx_prev_b = frame_idx_curr_b - self.world_size + elif track_in_reverse and frame_idx_curr_e < num_frames: + frame_idx_prev_b = frame_idx_curr_e + frame_idx_prev_e = min(frame_idx_prev_b + self.world_size, num_frames) + else: + frame_idx_prev_b = frame_idx_prev_e = None + if frame_idx_prev_b is not None: + for frame_idx_rm in range(frame_idx_prev_b, frame_idx_prev_e): + multigpu_buffer.pop(frame_idx_rm, None) + + # Step 3: compute and cache FA outputs of the next chunk ahead of time + # (so that we can overlap computation with all-gather transfer) + # Respect tracking bounds when calculating next chunk + + if not track_in_reverse and frame_idx_curr_e < valid_frame_end: + frame_idx_next_b = frame_idx_curr_e + frame_idx_next_e = min(frame_idx_next_b + self.world_size, valid_frame_end) + elif ( + track_in_reverse and frame_idx_curr_b - self.world_size >= valid_frame_start + ): + frame_idx_next_e = frame_idx_curr_b + frame_idx_next_b = max( + frame_idx_curr_b - self.world_size, valid_frame_start + ) + else: + frame_idx_next_b = frame_idx_next_e = None + if frame_idx_next_b is not None and frame_idx_next_b not in multigpu_buffer: + with torch.profiler.record_function("build_multigpu_buffer_next_chunk2"): + self._build_multigpu_buffer_next_chunk( + backbone_out=backbone_out, + find_inputs=find_inputs, + geometric_prompt=geometric_prompt, + frame_idx_begin=frame_idx_next_b, + frame_idx_end=frame_idx_next_e, + num_frames=num_frames, + multigpu_buffer=multigpu_buffer, + run_nms=run_nms, + nms_prob_thresh=nms_prob_thresh, + nms_iou_thresh=nms_iou_thresh, + feature_cache=feature_cache, + ) + + return out, backbone_out + + def _build_multigpu_buffer_next_chunk( + self, + backbone_out, + find_inputs, + geometric_prompt: Prompt, + frame_idx_begin, + frame_idx_end, + num_frames, + multigpu_buffer, + run_nms=False, + nms_prob_thresh=None, + nms_iou_thresh=None, + nms_use_iom=False, + feature_cache=None, + ): + """Compute FA outputs on a chunk of frames and store their results in multigpu_buffer.""" + # each GPU computes FA on one frame in the chunk (in a round-robin manner) + frame_idx_local_gpu = min(frame_idx_begin + self.rank, frame_idx_end - 1) + # `forward_grounding` (from base class `Sam3MultiplexImageBase`) runs FA on a single frame + with torch.profiler.record_function("forward_grounding"): + out_local = self.forward_grounding( + backbone_out=backbone_out, + # HACK: Since find_inputs is on GPU having to realloc is expensive so changing the values in place for the prod usecase + # i.e. when using the streaming frame loader resource instead of local file. For non-prod is always + # frame_idx_local_gpu < len(find_inputs) so should be a no-op + find_input=find_inputs[frame_idx_local_gpu % len(find_inputs)], + find_target=None, + geometric_prompt=geometric_prompt, + feature_cache=feature_cache, + ) + if run_nms: + with torch.profiler.record_function("nms_masks"): + # run NMS as a post-processing step on top of the detection outputs + assert nms_prob_thresh is not None and nms_iou_thresh is not None + pred_probs = out_local["pred_logits"].squeeze(-1).sigmoid() + pred_masks = out_local["pred_masks"] + # loop over text prompts (not an overhead for demo where there's only 1 prompt) + for prompt_idx in range(pred_probs.size(0)): + keep = nms_masks( + pred_probs=pred_probs[prompt_idx], + pred_masks=pred_masks[prompt_idx], + prob_threshold=nms_prob_thresh, + iou_threshold=nms_iou_thresh, + nms_use_iom=nms_use_iom, + do_compile=getattr(self, "compile_model", False), + running_in_prod=getattr(self, "running_in_prod", False), + ) + # set a very low threshold for those detections removed by NMS + out_local["pred_logits"][prompt_idx, :, 0] -= 1e4 * (~keep).float() + + if self.gather_backbone_out: + # gather the SAM 2 backbone features across GPUs + if self.is_multiplex: + # Note that we should not need to compute the interaction features every frame + # TODO: rooms for optimization + + # Interaction features + inte_feats = out_local["prev_encoder_out"]["backbone_out"][ + "interactive" + ] + assert inte_feats["vision_mask"] is None + assert ( + len(inte_feats["backbone_fpn"]) == 3 + ) # SAM2 backbone always have 3 levels + assert all(x.mask is None for x in inte_feats["backbone_fpn"]) + # cast the SAM2 backbone features to bfloat16 for all-gather (this is usually + # a no-op, SAM2 backbone features are likely already in bfloat16 due to AMP) + inte_backbone_fpn_bf16 = [ + x.to(torch.bfloat16) for x in inte_feats["backbone_fpn"] + ] + inte_fpn0, inte_fpn_handle0 = self._gather_tensor( + inte_backbone_fpn_bf16[0].tensors + ) + inte_fpn1, inte_fpn_handle1 = self._gather_tensor( + inte_backbone_fpn_bf16[1].tensors + ) + inte_fpn2, inte_fpn_handle2 = self._gather_tensor( + inte_backbone_fpn_bf16[2].tensors + ) + # vision_pos_enc is the same on all frames, so no need to all-gather them + inte_vision_pos_enc = inte_feats["vision_pos_enc"] + + feats = out_local["prev_encoder_out"]["backbone_out"]["sam2_backbone_out"] + assert feats["vision_mask"] is None + assert len(feats["backbone_fpn"]) == 3 # SAM2 backbone always have 3 levels + assert all(x.mask is None for x in feats["backbone_fpn"]) + # cast the SAM2 backbone features to bfloat16 for all-gather (this is usually + # a no-op, SAM2 backbone features are likely already in bfloat16 due to AMP) + backbone_fpn_bf16 = [x.to(torch.bfloat16) for x in feats["backbone_fpn"]] + fpn0, fpn_handle0 = self._gather_tensor(backbone_fpn_bf16[0].tensors) + fpn1, fpn_handle1 = self._gather_tensor(backbone_fpn_bf16[1].tensors) + fpn2, fpn_handle2 = self._gather_tensor(backbone_fpn_bf16[2].tensors) + # vision_pos_enc is the same on all frames, so no need to all-gather them + vision_pos_enc = feats["vision_pos_enc"] + + # trim the FA output to only include the necessary keys + out_local = { + "pred_logits": out_local["pred_logits"], + "pred_boxes": out_local["pred_boxes"], + "pred_boxes_xyxy": out_local["pred_boxes_xyxy"], + "pred_masks": out_local["pred_masks"], + "pred_object_ids": self._get_dummy_object_ids(out_local["pred_logits"]), + } + + # gather the results: after this step, each GPU will receive FA outputs on + # all frames in the chunk and store them in `multigpu_buffer` + out_gathered = {k: self._gather_tensor(v) for k, v in out_local.items()} + for rank in range(self.world_size): + frame_idx_to_save = frame_idx_begin + rank + if frame_idx_to_save >= num_frames: + continue + frame_buffer = { + k: (v[rank], handle) for k, (v, handle) in out_gathered.items() + } + if self.gather_backbone_out: + # also add gathered SAM 2 backbone features to frame_buffer + if self.is_multiplex: + frame_buffer["interactive_backbone_fpn_0"] = ( + inte_fpn0[rank], + inte_fpn_handle0, + ) + frame_buffer["interactive_backbone_fpn_1"] = ( + inte_fpn1[rank], + inte_fpn_handle1, + ) + frame_buffer["interactive_backbone_fpn_2"] = ( + inte_fpn2[rank], + inte_fpn_handle2, + ) + frame_buffer["interactive_backbone_pos_enc"] = ( + inte_vision_pos_enc, + None, + ) + frame_buffer["sam2_backbone_fpn_0"] = (fpn0[rank], fpn_handle0) + frame_buffer["sam2_backbone_fpn_1"] = (fpn1[rank], fpn_handle1) + frame_buffer["sam2_backbone_fpn_2"] = (fpn2[rank], fpn_handle2) + frame_buffer["sam2_backbone_pos_enc"] = (vision_pos_enc, None) + + multigpu_buffer[frame_idx_to_save] = frame_buffer + + def _gather_tensor(self, x): + if self.world_size == 1: + return [x], None + + async_op = self.async_all_gather + # here `.contiguous()` is required -- otherwise NCCL all_gather + # sometimes gives wrong results (based on Ronghang's observations) + x = x.contiguous() # ensure contiguous memory for NCCL + output_list = [torch.empty_like(x) for _ in range(self.world_size)] + handle = torch.distributed.all_gather(output_list, x, async_op=async_op) + return output_list, handle + + def forward_video_grounding_batched_multigpu( + self, + backbone_out, + find_inputs, + geometric_prompt: Prompt, + frame_idx, + num_frames, + # `grounding_cache` is a dict to cache FA outputs in a chunk between different calls + grounding_cache, + track_in_reverse=False, + # whether to also return the SAM2 backbone features (in addition to FA results) + return_sam2_backbone_feats=False, + # whether to perform NMS and suppress the scores of those detections removed by NMS + run_nms=False, + nms_prob_thresh=None, + nms_iou_thresh=None, + nms_use_iom=False, + # tracking bounds to respect max_frame_num_to_track + max_frame_num_to_track=None, + propagate_in_video_start_frame_idx=None, + # feature_cache for buffered backbone computation + feature_cache=None, + # batch_size for batched forward_grounding (default: 16) + batch_size=16, + ): + """ + Fully batched forward_grounding that processes chunks of frames together on each GPU. + + Unlike forward_video_grounding_multigpu which processes 1 frame per GPU per chunk, + this method processes `batch_size` frames at once using the batched forward_grounding + approach from Sam3MultiplexImageBase. + + For single-GPU (world_size=1), this is equivalent to forward_grounding_batched. + For multi-GPU, each GPU processes batch_size frames in parallel. + + Args: + backbone_out: Dictionary containing backbone outputs and image batch. + find_inputs: List of FindStage objects for all frames. + geometric_prompt: Prompt object (used as template, individual prompts are + constructed from find_inputs for batching). + frame_idx: Current frame index to process. + num_frames: Total number of frames in the video. + grounding_cache: Dictionary to cache grounding outputs. + track_in_reverse: If True, processing in reverse frame order. + return_sam2_backbone_feats: Whether to also return SAM2 backbone features. + run_nms: Whether to perform NMS on detection outputs. + nms_prob_thresh: Probability threshold for NMS. + nms_iou_thresh: IoU threshold for NMS. + nms_use_iom: Whether to use IoM for NMS. + max_frame_num_to_track: Maximum number of frames to track. + propagate_in_video_start_frame_idx: Start frame index for propagation. + feature_cache: Optional dictionary for backbone feature caching. + batch_size: Number of frames to batch together per GPU (default: 16). + + Returns: + Tuple of (out, backbone_out) where out contains detection results for frame_idx. + """ + # Calculate valid frame range based on max_frame_num_to_track + if max_frame_num_to_track is not None: + if propagate_in_video_start_frame_idx is None: + propagate_in_video_start_frame_idx = 0 + if track_in_reverse: + valid_frame_start = ( + propagate_in_video_start_frame_idx - max_frame_num_to_track + 1 + ) + valid_frame_end = propagate_in_video_start_frame_idx + else: + valid_frame_start = propagate_in_video_start_frame_idx + valid_frame_end = ( + propagate_in_video_start_frame_idx + max_frame_num_to_track + ) + else: + valid_frame_start = 0 + valid_frame_end = num_frames + + # Initialize grounding_buffer if not present + if "grounding_buffer" not in grounding_cache: + grounding_cache["grounding_buffer"] = {} + + # Calculate chunk boundaries - use batch_size instead of world_size + chunk_start = (frame_idx // batch_size) * batch_size + chunk_end = min(chunk_start + batch_size, valid_frame_end) + chunk_key = (chunk_start, chunk_end) + + # Process chunk if not already cached + if chunk_key not in grounding_cache["grounding_buffer"]: + with torch.profiler.record_function( + "forward_grounding_batched.process_chunk" + ): + chunk_outputs = self._process_grounding_chunk_batched( + backbone_out=backbone_out, + find_inputs=find_inputs, + chunk_start=chunk_start, + chunk_end=chunk_end, + run_nms=run_nms, + nms_prob_thresh=nms_prob_thresh, + nms_iou_thresh=nms_iou_thresh, + nms_use_iom=nms_use_iom, + feature_cache=feature_cache, + return_sam2_backbone_feats=return_sam2_backbone_feats, + ) + grounding_cache["grounding_buffer"][chunk_key] = chunk_outputs + + # Auto-cleanup previous chunks + self._cleanup_previous_chunks_multigpu( + grounding_cache=grounding_cache, + current_chunk_key=chunk_key, + batch_size=batch_size, + num_frames=num_frames, + track_in_reverse=track_in_reverse, + ) + + # Retrieve the cached output for this frame + chunk_outputs = grounding_cache["grounding_buffer"][chunk_key] + local_idx = frame_idx - chunk_start + + # Slice out the output for this specific frame + out = self._slice_batched_output( + chunk_outputs, local_idx, return_sam2_backbone_feats + ) + + return out, backbone_out + + def _process_grounding_chunk_batched( + self, + backbone_out, + find_inputs, + chunk_start: int, + chunk_end: int, + run_nms: bool, + nms_prob_thresh, + nms_iou_thresh, + nms_use_iom: bool, + feature_cache, + return_sam2_backbone_feats: bool, + ): + """ + Process a chunk of frames through the full forward_grounding pipeline in batch. + """ + chunk_size = chunk_end - chunk_start + + # Build geometric prompts for the chunk + chunk_geo_prompts = [ + self._get_geo_prompt_from_find_input(find_inputs[i % len(find_inputs)]) + for i in range(chunk_start, chunk_end) + ] + + # Batch the find_inputs for this chunk + batched_find_input = self._batch_find_inputs( + find_inputs, chunk_start, chunk_end + ) + + # Batch the geometric prompts + batched_geometric_prompt = self._batch_geometric_prompts_from_list( + chunk_geo_prompts + ) + + # Run forward_grounding on the batched input + with torch.profiler.record_function("forward_grounding_batched.forward"): + out = self.forward_grounding( + backbone_out=backbone_out, + find_input=batched_find_input, + find_target=None, + geometric_prompt=batched_geometric_prompt, + feature_cache=feature_cache, + ) + + # Apply NMS per frame in the batch + if run_nms: + with torch.profiler.record_function("forward_grounding_batched.nms"): + assert nms_prob_thresh is not None and nms_iou_thresh is not None + pred_probs = out["pred_logits"].squeeze(-1).sigmoid() + pred_masks = out["pred_masks"] + # pred_probs shape: [batch_size, num_queries] + # pred_masks shape: [batch_size, num_queries, H, W] + # Use batched NMS to process all frames at once + keep = nms_masks( + pred_probs=pred_probs, + pred_masks=pred_masks, + prob_threshold=nms_prob_thresh, + iou_threshold=nms_iou_thresh, + nms_use_iom=nms_use_iom, + do_compile=getattr(self, "compile_model", False), + running_in_prod=getattr(self, "running_in_prod", False), + ) + # Set a very low threshold for detections removed by NMS + # keep shape: [batch_size, num_queries] + out["pred_logits"][:, :, 0] -= 1e4 * (~keep).float() + + # Extract SAM2 backbone features if requested + if return_sam2_backbone_feats and "prev_encoder_out" in out: + backbone_data = out["prev_encoder_out"]["backbone_out"] + if self.is_multiplex and "interactive" in backbone_data: + out["_interactive_backbone"] = backbone_data["interactive"] + if "sam2_backbone_out" in backbone_data: + out["_sam2_backbone"] = backbone_data["sam2_backbone_out"] + + out["_chunk_size"] = chunk_size + return out + + def _slice_batched_output( + self, + chunk_outputs, + local_idx: int, + return_sam2_backbone_feats: bool, + ): + """ + Slice a single frame's output from the batched chunk outputs. + """ + out = {} + + # Keys to slice at batch dimension + batch_dim_keys = { + "pred_logits", + "pred_boxes", + "pred_boxes_xyxy", + "pred_masks", + "pred_logits_o2m", + "pred_boxes_o2m", + "pred_boxes_xyxy_o2m", + "pred_masks_o2m", + "queries", + "presence_logit_dec", + } + + # Keys to skip + skip_keys = { + "_chunk_size", + "_interactive_backbone", + "_sam2_backbone", + "prev_encoder_out", + "encoder_hidden_states", + "aux_outputs", + } + + for key, value in chunk_outputs.items(): + if key in skip_keys: + continue + if key in batch_dim_keys and isinstance(value, torch.Tensor): + out[key] = value[local_idx : local_idx + 1] + elif isinstance(value, torch.Tensor): + try: + out[key] = value[local_idx : local_idx + 1] + except (IndexError, RuntimeError): + out[key] = value + + # Add object IDs + if "pred_logits" in out: + out["pred_object_ids"] = self._get_dummy_object_ids(out["pred_logits"]) + + # Add SAM2 backbone features if requested + if return_sam2_backbone_feats: + if "_sam2_backbone" in chunk_outputs: + sam2_bb = chunk_outputs["_sam2_backbone"] + out["sam2_backbone_fpn_0"] = sam2_bb["backbone_fpn"][0].tensors[ + local_idx : local_idx + 1 + ] + out["sam2_backbone_fpn_1"] = sam2_bb["backbone_fpn"][1].tensors[ + local_idx : local_idx + 1 + ] + out["sam2_backbone_fpn_2"] = sam2_bb["backbone_fpn"][2].tensors[ + local_idx : local_idx + 1 + ] + out["sam2_backbone_pos_enc"] = [ + x[local_idx : local_idx + 1] for x in sam2_bb["vision_pos_enc"] + ] + + if self.is_multiplex and "_interactive_backbone" in chunk_outputs: + inte_bb = chunk_outputs["_interactive_backbone"] + out["interactive_backbone_fpn_0"] = inte_bb["backbone_fpn"][0].tensors[ + local_idx : local_idx + 1 + ] + out["interactive_backbone_fpn_1"] = inte_bb["backbone_fpn"][1].tensors[ + local_idx : local_idx + 1 + ] + out["interactive_backbone_fpn_2"] = inte_bb["backbone_fpn"][2].tensors[ + local_idx : local_idx + 1 + ] + out["interactive_backbone_pos_enc"] = [ + x[local_idx : local_idx + 1] for x in inte_bb["vision_pos_enc"] + ] + + return out + + def _cleanup_previous_chunks_multigpu( + self, + grounding_cache, + current_chunk_key, + batch_size: int, + num_frames: int, + track_in_reverse: bool, + ): + """Remove previous chunks from cache to save GPU memory.""" + chunk_start, chunk_end = current_chunk_key + + if not track_in_reverse: + prev_chunk_start = chunk_start - batch_size + if prev_chunk_start >= 0: + prev_chunk_end = chunk_start + prev_chunk_key = (prev_chunk_start, prev_chunk_end) + + # Cleanup grounding_buffer entry + chunk = grounding_cache["grounding_buffer"].pop(prev_chunk_key, None) + if chunk is not None: + del chunk + else: + next_chunk_start = chunk_end + if next_chunk_start < num_frames: + next_chunk_end = min(next_chunk_start + batch_size, num_frames) + next_chunk_key = (next_chunk_start, next_chunk_end) + grounding_cache["grounding_buffer"].pop(next_chunk_key, None) diff --git a/sam3/model/sam3_multiplex_detector_utils.py b/sam3/model/sam3_multiplex_detector_utils.py new file mode 100644 index 0000000..26eb9a9 --- /dev/null +++ b/sam3/model/sam3_multiplex_detector_utils.py @@ -0,0 +1,369 @@ +import logging + +import numpy as np +import torch +from sam3 import perflib + +try: + # Ronghang's generic GPU NMS implementation; install via + # pip uninstall -y torch_generic_nms; TORCH_CUDA_ARCH_LIST="8.0 9.0" pip install git+https://github.com/ronghanghu/torch_generic_nms + from torch_generic_nms import generic_nms + + GENERIC_NMS_AVAILABLE = True +except ImportError: + GENERIC_NMS_AVAILABLE = False + +from sam3.perflib.masks_ops import mask_iou +from sam3.train.masks_ops import mask_iom + + +def nms_masks( + pred_probs: torch.Tensor, + pred_masks: torch.Tensor, + prob_threshold: float, + iou_threshold: float, + nms_use_iom: bool = False, + do_compile: bool = False, + running_in_prod: bool = False, +) -> torch.Tensor: + """ + Args: + - pred_probs: (num_det,) or (B, num_det) float Tensor, containing the score (probability) of each detection + - pred_masks: (num_det, H_mask, W_mask) or (B, num_det, H_mask, W_mask) float Tensor, containing the binary segmentation mask of each detection + - prob_threshold: float, score threshold to prefilter detections (NMS is performed on detections above threshold) + - iou_threshold: float, mask IoU threshold for NMS (it would also be used as IoM threshold if `nms_use_iom` is True) + - nms_use_iom: bool, if True, use IoM instead of IoU for NMS + - do_compile: bool, whether to compile the function for optimization + - running_in_prod: bool, whether the function is running in production (ie, in Instagram) + + Returns: + - keep: (num_det,) or (B, num_det) bool Tensor, indicating whether each detection is kept after score thresholding + NMS + """ + if do_compile and perflib.is_enabled: + # Apply torch.compile with the same settings as before + compiled_fn = torch.compile( + _nms_masks_core, + mode="max-autotune", + fullgraph=True, + # dynamic=False, + ) + return compiled_fn( + pred_probs, pred_masks, prob_threshold, iou_threshold, nms_use_iom + ) + else: + return _nms_masks_core( + pred_probs, pred_masks, prob_threshold, iou_threshold, nms_use_iom + ) + + +def _nms_masks_core( + pred_probs: torch.Tensor, + pred_masks: torch.Tensor, + prob_threshold: float, + iou_threshold: float, + nms_use_iom: bool = False, +) -> torch.Tensor: + """Core NMS implementation without compilation. + + Supports both single-frame and batched inputs: + - Single-frame: pred_probs (num_det,), pred_masks (num_det, H, W) + - Batched: pred_probs (B, num_det), pred_masks (B, num_det, H, W) + + Returns: + - keep: bool Tensor with same leading dimensions as input, indicating kept detections + """ + # Check if input is batched (has batch dimension) + is_batched = pred_probs.dim() == 2 + + if is_batched: + return _nms_masks_core_batched( + pred_probs, pred_masks, prob_threshold, iou_threshold, nms_use_iom + ) + else: + # Single-frame input: use original logic + return _nms_masks_core_single( + pred_probs, pred_masks, prob_threshold, iou_threshold, nms_use_iom + ) + + +def _nms_masks_core_batched( + pred_probs: torch.Tensor, + pred_masks: torch.Tensor, + prob_threshold: float, + iou_threshold: float, + nms_use_iom: bool = False, +) -> torch.Tensor: + """Core NMS implementation for batched inputs using vectorized operations. + + Args: + - pred_probs: (B, num_det) float Tensor + - pred_masks: (B, num_det, H_mask, W_mask) float Tensor + - prob_threshold: float, score threshold to prefilter detections + - iou_threshold: float, mask IoU/IoM threshold for NMS + - nms_use_iom: bool, if True, use IoM instead of IoU for NMS + + Returns: + - keep: (B, num_det) bool Tensor + """ + B, num_det, H, W = pred_masks.shape + device = pred_masks.device + + is_valid = pred_probs > prob_threshold # (B, num_det) + masks_binary = pred_masks > 0 # (B, num_det, H, W) + + if perflib.is_enabled: + # Compute batched pairwise IoU/IoM + if nms_use_iom: + overlaps = _batched_mask_iom(masks_binary) # (B, num_det, num_det) + else: + overlaps = _batched_mask_iou(masks_binary) # (B, num_det, num_det) + keep = _batched_generic_nms_mask(overlaps, pred_probs, is_valid, iou_threshold) + return keep + + # Non-perflib path: compute batched IoU/IoM + if nms_use_iom: + overlaps = _batched_mask_iom(masks_binary) # (B, num_det, num_det) + else: + overlaps = _batched_mask_iou(masks_binary) # (B, num_det, num_det) + + # Apply batched NMS + keep = _batched_generic_nms_mask(overlaps, pred_probs, is_valid, iou_threshold) + return keep + + +def _batched_mask_iou(masks: torch.Tensor) -> torch.Tensor: + """Compute batched pairwise IoU for masks. + + Args: + - masks: (B, N, H, W) bool Tensor + + Returns: + - ious: (B, N, N) float Tensor + """ + B, N, H, W = masks.shape + # Flatten spatial dims: (B, N, H*W) + masks_flat = masks.reshape(B, N, -1).float() + + # Compute intersection via batched matrix multiplication: (B, N, N) + intersection = torch.bmm(masks_flat, masks_flat.transpose(1, 2)) + + # Compute areas: (B, N) + areas = masks_flat.sum(dim=-1) + + # Compute union: (B, N, N) + union = areas.unsqueeze(2) + areas.unsqueeze(1) - intersection + + return intersection / (union + 1e-8) + + +def _batched_mask_iom(masks: torch.Tensor) -> torch.Tensor: + """Compute batched pairwise IoM (Intersection over Minimum) for masks. + + Args: + - masks: (B, N, H, W) bool Tensor + + Returns: + - ioms: (B, N, N) float Tensor + """ + B, N, H, W = masks.shape + # Flatten spatial dims: (B, N, H*W) + masks_flat = masks.reshape(B, N, -1).float() + + # Compute intersection via batched matrix multiplication: (B, N, N) + intersection = torch.bmm(masks_flat, masks_flat.transpose(1, 2)) + + # Compute areas: (B, N) + areas = masks_flat.sum(dim=-1) + + # Compute min area: (B, N, N) + min_area = torch.minimum(areas.unsqueeze(2), areas.unsqueeze(1)) + + return intersection / (min_area + 1e-8) + + +def _batched_generic_nms_mask( + ious: torch.Tensor, + scores: torch.Tensor, + is_valid: torch.Tensor, + iou_threshold: float, +) -> torch.Tensor: + """Batched NMS using vectorized operations. + + Args: + - ious: (B, N, N) float Tensor, pairwise IoU/IoM matrix + - scores: (B, N) float Tensor, detection scores + - is_valid: (B, N) bool Tensor, valid detections mask + - iou_threshold: float, threshold for suppression + + Returns: + - keep: (B, N) bool Tensor + """ + B, N = scores.shape + device = scores.device + + # Sort by score descending for each batch: (B, N) + order = scores.argsort(dim=-1, descending=True) + + # Create batch indices for advanced indexing + batch_idx = torch.arange(B, device=device).unsqueeze(1).expand(B, N) + + # Reorder IoU matrix according to sorted scores: (B, N, N) + # ious_sorted[b, i, j] = ious[b, order[b, i], order[b, j]] + ious_sorted = ious[batch_idx.unsqueeze(2), order.unsqueeze(2), order.unsqueeze(1)] + + # Create threshold mask: (B, N, N) + threshold_mask = ious_sorted > iou_threshold + + # Initialize keep mask with valid detections in sorted order: (B, N) + keep = is_valid[batch_idx, order] + + # Upper triangular mask to avoid double processing: (N, N) + triu = torch.triu(torch.ones(N, N, device=device, dtype=torch.bool), diagonal=1) + + # Vectorized NMS - iterate through detections + for i in range(N): + # For each position i, suppress later detections with high overlap + # Only suppress if current detection is kept + suppress = ( + threshold_mask[:, i, :] & triu[i].unsqueeze(0) & keep[:, i].unsqueeze(1) + ) + keep = keep & ~suppress + + # Return keep mask in original order: (B, N) + original_keep = torch.zeros_like(keep) + original_keep[batch_idx, order] = keep + return original_keep + + +def _nms_masks_core_single( + pred_probs: torch.Tensor, + pred_masks: torch.Tensor, + prob_threshold: float, + iou_threshold: float, + nms_use_iom: bool = False, +) -> torch.Tensor: + """Core NMS implementation for a single frame (no batch dimension). + + Args: + - pred_probs: (num_det,) float Tensor + - pred_masks: (num_det, H_mask, W_mask) float Tensor + - prob_threshold: float, score threshold to prefilter detections + - iou_threshold: float, mask IoU/IoM threshold for NMS + - nms_use_iom: bool, if True, use IoM instead of IoU for NMS + + Returns: + - keep: (num_det,) bool Tensor + """ + is_valid = pred_probs > prob_threshold # (num_det,) + + if perflib.is_enabled: + masks_binary = pred_masks > 0 # (num_det, H_mask, W_mask) + if nms_use_iom: + ious = perf_mask_iom(masks_binary, masks_binary) # (num_det, num_det) + else: + ious = perf_mask_iou(masks_binary, masks_binary) # (num_det, num_det) + kept_mask = generic_nms_mask(ious, pred_probs, is_valid, iou_threshold) + return kept_mask + # prefilter the detections with prob_threshold ("valid" are those above prob_threshold) + probs = pred_probs[is_valid] # (num_valid,) + masks_binary = pred_masks[is_valid] > 0 # (num_valid, H_mask, W_mask) + if probs.numel() == 0: + return is_valid # no valid detection, return empty keep mask + + if nms_use_iom: + overlaps = mask_iom(masks_binary, masks_binary) # (num_valid, num_valid) + else: + overlaps = mask_iou(masks_binary, masks_binary) # (num_valid, num_valid) + # kept_inds are the indices among `probs` of those kept detections after NMS + if GENERIC_NMS_AVAILABLE: + kept_inds = generic_nms(overlaps, probs, iou_threshold, use_iou_matrix=True) + else: + logging.warning( + "Falling back to CPU mask NMS implementation -- please install `torch_generic_nms` via\n\t" + 'pip uninstall -y torch_generic_nms; TORCH_CUDA_ARCH_LIST="8.0 9.0" pip install git+https://github.com/ronghanghu/torch_generic_nms' + ) + kept_inds = generic_nms_cpu(overlaps, probs, iou_threshold) + + # valid_inds are the indices among `probs` of valid detections before NMS (or -1 for invalid) + valid_inds = torch.where(is_valid, is_valid.cumsum(dim=0) - 1, -1) # (num_det,) + keep = torch.isin(valid_inds, kept_inds) # (num_det,) + return keep + + +def generic_nms_cpu( + ious: torch.Tensor, scores: torch.Tensor, iou_threshold=0.5 +) -> torch.Tensor: + """ + A generic version of `torchvision.ops.nms` that takes a pairwise IoU matrix. (CPU implementation + based on https://github.com/jwyang/faster-rcnn.pytorch/blob/master/lib/model/nms/nms_cpu.py) + """ + ious_np = ious.float().detach().cpu().numpy() + scores_np = scores.float().detach().cpu().numpy() + order = scores_np.argsort()[::-1] + kept_inds = [] + while order.size > 0: + i = order.item(0) + kept_inds.append(i) + inds = np.where(ious_np[i, order[1:]] <= iou_threshold)[0] + order = order[inds + 1] + + return torch.tensor(kept_inds, dtype=torch.int64, device=scores.device) + + +def generic_nms_mask( + ious: torch.Tensor, scores: torch.Tensor, is_valid: torch.Tensor, iou_threshold=0.5 +) -> torch.Tensor: + """ + A generic version of `torchvision.ops.nms` that takes a pairwise IoU matrix. (CPU implementation + using vectorized operations similar to nms_masks_kernel) + """ + # Sort by score descending + order = scores.argsort(descending=True) + + # Reorder IoU matrix according to sorted scores + ious_sorted = ious[order][:, order] + + # Create threshold mask + threshold_mask = ious_sorted > iou_threshold + + # Initialize keep mask + # keep = torch.ones(len(scores), device=ious.device, dtype=torch.bool) + keep = is_valid[order] + + # Upper triangular mask to avoid double processing + tr = torch.triu(torch.ones_like(threshold_mask), diagonal=1) + + # Vectorized NMS + for i in range(len(scores)): + # Suppress all boxes that have high IoU with current box + m = threshold_mask[i] + keep = torch.where(m & tr[i], torch.zeros_like(keep), keep) + + # Return keep mask in original order + original_keep = torch.zeros_like(keep) + original_keep[order] = keep + return original_keep + + +def perf_mask_iou(pred_masks: torch.Tensor, gt_masks: torch.Tensor) -> torch.Tensor: + """ + Compute the IoU (Intersection over Union) between predicted masks and ground truth masks. + + Args: + - pred_masks: (N, H, W) bool Tensor, containing binary predicted segmentation masks + - gt_masks: (M, H, W) bool Tensor, containing binary ground truth segmentation masks + + Returns: + - ious: (N, M) float Tensor, containing IoUs for each pair of predicted and ground truth masks + """ + assert pred_masks.dtype == gt_masks.dtype == torch.bool + from sam3.perflib.iou import pairwise_iou + + return pairwise_iou(pred_masks, gt_masks, eps=None) + + +def perf_mask_iom(pred_masks: torch.Tensor, gt_masks: torch.Tensor) -> torch.Tensor: + assert pred_masks.dtype == gt_masks.dtype == torch.bool + from sam3.perflib.iou import pairwise_iom + + return pairwise_iom(pred_masks, gt_masks) diff --git a/sam3/model/sam3_multiplex_tracking.py b/sam3/model/sam3_multiplex_tracking.py new file mode 100644 index 0000000..d722e1a --- /dev/null +++ b/sam3/model/sam3_multiplex_tracking.py @@ -0,0 +1,3431 @@ +from collections import defaultdict +from functools import reduce +from typing import Dict + +import numpy as np +import sam3.model.sam3_multiplex_base +import sam3.model.sam3_video_base +import torch +import torch.distributed as dist +import torch.nn.functional as F +from sam3 import perflib +from sam3.logger import get_logger +from sam3.model.box_ops import box_xywh_to_cxcywh, box_xyxy_to_xywh +from sam3.model.data_misc import BatchedDatapoint +from sam3.model.sam3_multiplex_base import MaskletConfirmationStatus, Sam3MultiplexBase +from sam3.model.sam3_tracker_utils import fill_holes_in_mask_scores +from sam3.model.sam3_video_inference import is_image_type +from sam3.perflib.compile import ( + clone_output_wrapper, + compile_wrapper, + shape_logging_wrapper, +) +from sam3.perflib.masks_ops import mask_iou, masks_to_boxes as perf_masks_to_boxes +from torch import Tensor +from torchvision.ops import masks_to_boxes +from tqdm.auto import tqdm + +logger = get_logger(__name__) + +import gc +from collections.abc import Mapping, Sequence +from dataclasses import fields, is_dataclass +from typing import List + +from sam3.model.data_misc import ( + BatchedPointer, + convert_my_tensors, + FindStage, + NestedTensor, +) +from sam3.model.geometry_encoders import Prompt +from sam3.model.io_utils import load_resource_as_video_frames + + +def recursive_to(data, *args, **kwargs): + if isinstance(data, torch.Tensor): + ret = data.to(*args, **kwargs) + elif isinstance(data, np.ndarray): + ret = data + elif isinstance(data, Mapping): + ret = type(data)() + for key in data: + ret[key] = recursive_to(data[key], *args, **kwargs) + elif isinstance(data, tuple): + ret = () + for value in data: + ret += (recursive_to(value, *args, **kwargs),) + elif isinstance(data, Sequence) and not isinstance(data, str): + ret = type(data)() + for value in data: + ret.append(recursive_to(value, *args, **kwargs)) + elif is_dataclass(data): + ret_cls = type(data) + ret_fields = { + field.name: recursive_to(getattr(data, field.name), *args, **kwargs) + for field in fields(data) + } + ret = ret_cls(**ret_fields) + else: + ret = data + return ret + + +DUMMY_OUTPUT = "DUMMY_OUTPUT" + + +class Sam3MultiplexTracking(Sam3MultiplexBase): + def __init__( + self, + image_size=1008, + image_mean=(0.5, 0.5, 0.5), + image_std=(0.5, 0.5, 0.5), + compile_model=False, + postprocess_batch_size=1, + **kwargs, + ): + """ + hotstart_delay: int, the delay (in #frames) before the model starts to yield output, 0 to disable hotstart delay. + hotstart_unmatch_thresh: int, remove the object if it has this many unmatched frames within its hotstart_delay period. + If `hotstart_delay` is set to 0, this parameter is ignored. + hotstart_dup_thresh: int, remove the object if it has overlapped with another object this many frames within its hotstart_delay period. + postprocess_batch_size: int, the number of frames to accumulate before running postprocessing. Set to 1 to disable batching. + """ + super().__init__(**kwargs) + self.image_size = image_size + self.image_mean = image_mean + self.image_std = image_std + self.compile_model = compile_model + self.detector.compile_model = self.compile_model + self.postprocess_batch_size = postprocess_batch_size + + TEXT_ID_FOR_TEXT = 0 + TEXT_ID_FOR_VISUAL = 1 + TEXT_ID_FOR_GEOMETRIC = 2 + + def _construct_initial_input_batch(self, inference_state, images): + """Construct an initial `BatchedDatapoint` instance as input.""" + # 1) img_batch + num_frames = len(images) + device = inference_state["device"] + img_batch = NestedTensor(tensors=images, mask=None) + + # 2) find_text_batch + # "" will be replaced by the actual text prompt when adding prompts + find_text_batch = ["", "visual", "geometric"] + + # 3) find_inputs + input_box_embedding_dim = 258 # historical default + input_points_embedding_dim = 257 # historical default + dummy_ptrs = BatchedPointer( + stage_ids=[], query_ids=[], object_ids=[], ptr_mask=[], ptr_types=[] + ) + stages = [ + FindStage( + img_ids=[stage_id], + img_ids_np=np.array([stage_id]), + text_ids=[0], + input_boxes=[torch.zeros(input_box_embedding_dim)], + input_boxes_before_embed=[torch.empty(0, 4)], + input_boxes_mask=[torch.empty(0, dtype=torch.bool)], + input_boxes_label=[torch.empty(0, dtype=torch.long)], + input_points=[torch.empty(0, input_points_embedding_dim)], + input_points_before_embed=[torch.empty(0, 3)], + input_points_mask=[torch.empty(0)], + ptrs=dummy_ptrs, + ptrs_seg=dummy_ptrs, + object_ids=[], + ) + for stage_id in range(num_frames) + ] + with torch.profiler.record_function( + "Sam3MultiplexTracking._construct_initial_input_batch" + ): + for i in range(len(stages)): + stages[i] = convert_my_tensors(stages[i]) + + # construct the final `BatchedDatapoint` and cast to GPU + input_batch = BatchedDatapoint( + img_batch=img_batch, + find_text_batch=find_text_batch, + find_inputs=stages, + find_targets=[None] * num_frames, + get_queries=None, + find_metadatas=[None] * num_frames, + ) + with torch.profiler.record_function("Sam3MultiplexTracking.recursive_to"): + input_batch = recursive_to(input_batch, device, non_blocking=True) + inference_state["input_batch"] = input_batch + + # construct the placeholder interactive prompts and tracking queries + bs = 1 + inference_state["constants"]["empty_geometric_prompt"] = Prompt( + box_embeddings=torch.zeros(0, bs, 4, device=device), + box_mask=torch.zeros(bs, 0, device=device, dtype=torch.bool), + box_labels=torch.zeros(0, bs, device=device, dtype=torch.long), + point_embeddings=torch.zeros(0, bs, 2, device=device), + point_mask=torch.zeros(bs, 0, device=device, dtype=torch.bool), + point_labels=torch.zeros(0, bs, device=device, dtype=torch.long), + ) + + # constructing an output list in inference state (we start with an empty list) + inference_state["previous_stages_out"] = [None] * num_frames + inference_state["text_prompt"] = None + inference_state["per_frame_raw_point_input"] = [None] * num_frames + inference_state["per_frame_raw_box_input"] = [None] * num_frames + inference_state["per_frame_visual_prompt"] = [None] * num_frames + inference_state["per_frame_geometric_prompt"] = [None] * num_frames + inference_state["per_frame_cur_step"] = [0] * num_frames + + # placeholders for cached outputs + # (note: currently, a single visual prompt embedding is shared for all frames) + inference_state["backbone_out"] = None + inference_state["visual_prompt_embed"] = None + inference_state["visual_prompt_mask"] = None + + def _get_visual_prompt(self, inference_state, frame_idx, boxes_cxcywh, box_labels): + batch_size = 1 + geometric_prompt = Prompt( + box_embeddings=torch.zeros( + 0, batch_size, 4, device=inference_state["device"] + ), + box_mask=torch.zeros( + batch_size, 0, device=inference_state["device"], dtype=torch.bool + ), + point_embeddings=None, + point_mask=None, + ) + + geometric_prompt.append_boxes( + boxes=boxes_cxcywh.view(-1, batch_size, 4).to(inference_state["device"]), + labels=box_labels.view(-1, batch_size).to(inference_state["device"]), + ) + + return boxes_cxcywh, box_labels, geometric_prompt + + @torch.inference_mode() + def init_state( + self, + resource_path, + offload_video_to_cpu=False, + async_loading_frames=False, + use_torchcodec=False, + use_cv2=False, + input_is_mp4=False, + ): + # Initialize inference state (inlined from Sam3DemoMixin.init_state) + if use_torchcodec: + video_loader_type = "torchcodec" + elif use_cv2: + video_loader_type = "cv2" + else: + video_loader_type = "cv2" + images, orig_height, orig_width = load_resource_as_video_frames( + resource_path=resource_path, + image_size=self.image_size, + offload_video_to_cpu=offload_video_to_cpu, + img_mean=self.image_mean, + img_std=self.image_std, + async_loading_frames=async_loading_frames, + video_loader_type=video_loader_type, + ) + inference_state = {} + inference_state["image_size"] = self.image_size + inference_state["num_frames"] = len(images) + inference_state["device"] = torch.device("cuda") + inference_state["orig_height"] = orig_height + inference_state["orig_width"] = orig_width + inference_state["constants"] = {} + self._construct_initial_input_batch(inference_state, images) + # initialize extra states + # sam2_inference_states will contain separate inference_states for each frame having new objects if + # self.tracker.per_obj_inference is False (bucketized batching), or a single inference_state + # containing all objects if self.tracker.per_obj_inference is True (no batching at all). + inference_state["sam2_inference_states"] = [] + inference_state["tracker_metadata"] = {} + inference_state["feature_cache"] = {} + inference_state["cached_frame_outputs"] = {} + inference_state["is_image_only"] = is_image_type(resource_path) + return inference_state + + def reset_state(self, inference_state): + # Inlined from Sam3DemoMixin.reset_state + inference_state["input_batch"].find_text_batch[0] = "" + inference_state["text_prompt"] = None + for t in range(inference_state["num_frames"]): + inference_state["input_batch"].find_inputs[t].text_ids[...] = 0 + inference_state["previous_stages_out"][t] = None + inference_state["per_frame_raw_point_input"][t] = None + inference_state["per_frame_raw_box_input"][t] = None + inference_state["per_frame_visual_prompt"][t] = None + inference_state["per_frame_geometric_prompt"][t] = None + inference_state["per_frame_cur_step"][t] = 0 + inference_state["backbone_out"] = None + inference_state["visual_prompt_embed"] = None + inference_state["visual_prompt_mask"] = None + # reset extra states + inference_state["sam2_inference_states"].clear() + inference_state["tracker_metadata"].clear() + inference_state["feature_cache"].clear() + inference_state["cached_frame_outputs"] = {} + + def _get_processing_order( + self, inference_state, start_frame_idx, max_frame_num_to_track, reverse + ): + num_frames = inference_state["num_frames"] + previous_stages_out = inference_state["previous_stages_out"] + if all(out is None for out in previous_stages_out) and start_frame_idx is None: + raise RuntimeError( + "No prompts are received on any frames. Please add prompt on at least one frame before propagation." + ) + # set start index, end index, and processing order + if start_frame_idx is None: + # default: start from the earliest frame with input points + start_frame_idx = min( + t for t, out in enumerate(previous_stages_out) if out is not None + ) + if max_frame_num_to_track is None: + # default: track all the frames in the video + max_frame_num_to_track = num_frames + if reverse: + end_frame_idx = start_frame_idx - max_frame_num_to_track + end_frame_idx = max(end_frame_idx, 0) + processing_order = range(start_frame_idx - 1, end_frame_idx - 1, -1) + else: + end_frame_idx = start_frame_idx + max_frame_num_to_track + end_frame_idx = min(end_frame_idx, num_frames - 1) + processing_order = range(start_frame_idx, end_frame_idx + 1) + return processing_order, end_frame_idx + + @torch.inference_mode() + def propagate_in_video( + self, + inference_state, + start_frame_idx=None, + max_frame_num_to_track=None, + reverse=False, + output_prob_thresh=0.5, + compute_stability_score=False, + is_instance_processing=False, + **kwargs, # To support passing extra args to child classes + ): + """ + Propagate the prompts to get grounding results for the entire video. This method + is a generator and yields inference outputs for all frames in the range specified + by `start_frame_idx`, `max_frame_num_to_track`, and `reverse`. + """ + # compile the model (it's a no-op if the model is already compiled) + # note that it's intentionally added to `self.propagate_in_video`, so that the first + # `self.add_prompt` call will be done in eager mode to fill in the decoder buffers + # such as positional encoding cache) + self._compile_model() + + processing_order, end_frame_idx = self._get_processing_order( + inference_state, + start_frame_idx, + max_frame_num_to_track, + reverse=reverse, + ) + + # Store max_frame_num_to_track in feature_cache for downstream methods + inference_state["feature_cache"]["tracking_bounds"] = { + "max_frame_num_to_track": max_frame_num_to_track, + "propagate_in_video_start_frame_idx": start_frame_idx, + } + + hotstart_buffer = [] + hotstart_removed_obj_ids = set() + # when deciding whether to output a masklet on `yield_frame_idx`, we check whether the object is confirmed + # in a future frame (`unconfirmed_frame_delay` frames after the current frame). For example, if we require + # an object to be detected in 3 consecutive frames to be confirmed, then we look 2 frames in the future -- + # e.g., we output an object on frame 4 only if it becomes confirmed on frame 6. + unconfirmed_status_delay = self.masklet_confirmation_consecutive_det_thresh - 1 + unconfirmed_obj_ids_per_frame = {} # frame_idx -> hidden_obj_ids + + # Batch postprocessing: accumulate yield_list entries and process every postprocess_batch_size frames + postprocess_yield_list = [] + + for frame_idx in tqdm( + processing_order, desc="propagate_in_video", disable=self.rank > 0 + ): + out = self._run_single_frame_inference( + inference_state, + frame_idx, + reverse, + is_instance_processing=is_instance_processing, + ) + + if self.hotstart_delay > 0: + # accumulate the outputs for the first `hotstart_delay` frames + hotstart_buffer.append([frame_idx, out]) + # update the object IDs removed by hotstart so that we don't output them + if self.rank == 0: + hotstart_removed_obj_ids.update(out["removed_obj_ids"]) + unconfirmed_obj_ids = out.get("unconfirmed_obj_ids", None) + if unconfirmed_obj_ids is not None: + unconfirmed_obj_ids_per_frame[frame_idx] = unconfirmed_obj_ids + + if frame_idx == end_frame_idx: + # we reached the end of propagation -- yield all frames in the buffer + yield_list = hotstart_buffer + hotstart_buffer = [] + elif len(hotstart_buffer) >= self.hotstart_delay: + # we have enough frames -- yield and remove the first (oldest) frame from the buffer + yield_list = hotstart_buffer[:1] + hotstart_buffer = hotstart_buffer[1:] + else: + # not enough frames yet -- skip yielding + yield_list = [] + else: + yield_list = [(frame_idx, out)] # output the current frame + + # Accumulate yield_list into postprocess_yield_list + # Snapshot hotstart_removed_obj_ids at the time of accumulation to preserve + # the correct state for each frame (important: this set is mutated over time) + for yield_frame_idx, yield_out in yield_list: + postprocess_yield_list.append( + (yield_frame_idx, yield_out, set(hotstart_removed_obj_ids)) + ) + + # Process batch when we have enough frames + while len(postprocess_yield_list) >= self.postprocess_batch_size: + batch_to_process = postprocess_yield_list[: self.postprocess_batch_size] + postprocess_yield_list = postprocess_yield_list[ + self.postprocess_batch_size : + ] + + with torch.profiler.record_function( + "Sam3MultiplexTracking.postprocess_output_batched" + ): + if self.rank == 0: + # Prepare batched inputs for postprocessing + H_video, W_video = ( + inference_state["orig_height"], + inference_state["orig_width"], + ) + num_frames = inference_state["num_frames"] + + batched_outs = [] + frame_indices = [] + for ( + yield_frame_idx, + yield_out, + removed_obj_ids_snapshot, + ) in batch_to_process: + suppressed_obj_ids = yield_out["suppressed_obj_ids"] + unconfirmed_status_frame_idx = ( + yield_frame_idx + unconfirmed_status_delay + if not reverse + else yield_frame_idx - unconfirmed_status_delay + ) + unconfirmed_status_frame_idx = max( + 0, min(unconfirmed_status_frame_idx, num_frames - 1) + ) + unconfirmed_obj_ids = unconfirmed_obj_ids_per_frame.get( + unconfirmed_status_frame_idx, None + ) + + batched_outs.append( + ( + yield_out, + removed_obj_ids_snapshot, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) + ) + frame_indices.append(yield_frame_idx) + + # Cache frame outputs + self._cache_frame_outputs( + inference_state, + yield_frame_idx, + yield_out["obj_id_to_mask"], + suppressed_obj_ids=suppressed_obj_ids, + removed_obj_ids=removed_obj_ids_snapshot, + unconfirmed_obj_ids=unconfirmed_obj_ids, + ) + + if self.postprocess_batch_size > 1: + # Process all frames in batch + postprocessed_outs = self._postprocess_output_batched( + H_video, W_video, batched_outs + ) + else: + # Process each frame individually but output together + postprocessed_outs = [] + for ( + yield_out, + removed_obj_ids_snapshot, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) in batched_outs: + postprocessed_out = self._postprocess_output( + inference_state, + yield_out, + removed_obj_ids_snapshot, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) + postprocessed_outs.append(postprocessed_out) + + # Yield results + for yield_frame_idx, postprocessed_out in zip( + frame_indices, postprocessed_outs + ): + yield yield_frame_idx, postprocessed_out + else: + # No output on other GPUs + for yield_frame_idx, _, _ in batch_to_process: + yield yield_frame_idx, DUMMY_OUTPUT + + # Flush any remaining frames in the postprocess buffer + if len(postprocess_yield_list) > 0: + with torch.profiler.record_function( + "Sam3MultiplexTracking.postprocess_output_batched" + ): + if self.rank == 0: + H_video, W_video = ( + inference_state["orig_height"], + inference_state["orig_width"], + ) + num_frames = inference_state["num_frames"] + + batched_outs = [] + frame_indices = [] + for ( + yield_frame_idx, + yield_out, + removed_obj_ids_snapshot, + ) in postprocess_yield_list: + suppressed_obj_ids = yield_out["suppressed_obj_ids"] + unconfirmed_status_frame_idx = ( + yield_frame_idx + unconfirmed_status_delay + if not reverse + else yield_frame_idx - unconfirmed_status_delay + ) + unconfirmed_status_frame_idx = max( + 0, min(unconfirmed_status_frame_idx, num_frames - 1) + ) + unconfirmed_obj_ids = unconfirmed_obj_ids_per_frame.get( + unconfirmed_status_frame_idx, None + ) + + batched_outs.append( + ( + yield_out, + removed_obj_ids_snapshot, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) + ) + frame_indices.append(yield_frame_idx) + + self._cache_frame_outputs( + inference_state, + yield_frame_idx, + yield_out["obj_id_to_mask"], + suppressed_obj_ids=suppressed_obj_ids, + removed_obj_ids=removed_obj_ids_snapshot, + unconfirmed_obj_ids=unconfirmed_obj_ids, + ) + + if self.postprocess_batch_size > 1: + postprocessed_outs = self._postprocess_output_batched( + H_video, W_video, batched_outs + ) + else: + # Process each frame individually but output together + postprocessed_outs = [] + for ( + yield_out, + removed_obj_ids_snapshot, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) in batched_outs: + postprocessed_out = self._postprocess_output( + inference_state, + yield_out, + removed_obj_ids_snapshot, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) + postprocessed_outs.append(postprocessed_out) + + for yield_frame_idx, postprocessed_out in zip( + frame_indices, postprocessed_outs + ): + yield yield_frame_idx, postprocessed_out + else: + for yield_frame_idx, _, _ in postprocess_yield_list: + yield yield_frame_idx, DUMMY_OUTPUT + + if self.is_multiplex: + # log the bucket utilization stats + # bucket utilization rate is total valid objects / total capacity -> represents rooms for improvement + # subscription rate is total valid objects / total number of buckets -> represents speedup + total_valid_objects = 0 + total_num_buckets = 0 + for state in inference_state["sam2_inference_states"]: + assert ( + len(state["obj_ids"]) + == state["multiplex_state"].total_valid_entries + ) + total_valid_objects += len(state["obj_ids"]) + total_num_buckets += state["multiplex_state"].num_buckets + if total_num_buckets > 0: + bucket_utilization_rate = ( + total_valid_objects / (total_num_buckets * self.bucket_capacity) + ) * 100 + subscription_rate = (total_valid_objects / total_num_buckets) * 100 + logger.info( + f"Bucket utilization rate: {bucket_utilization_rate:.2f}%, subscription rate: {subscription_rate:.2f}%" + ) + + def _run_single_frame_inference( + self, + inference_state, + frame_idx, + reverse, + is_instance_processing=False, + ): + """ + Perform inference on a single frame and get its inference results. This would + also update `inference_state`. + """ + # prepare inputs + input_batch = inference_state["input_batch"] + tracker_states_local = inference_state["sam2_inference_states"] + geometric_prompt = ( + inference_state["constants"]["empty_geometric_prompt"] + if inference_state["per_frame_geometric_prompt"][frame_idx] is None + else inference_state["per_frame_geometric_prompt"][frame_idx] + ) + text_batch_key = tuple(input_batch.find_text_batch) + inference_state["feature_cache"]["text"] = { + text_batch_key: { + "language_features": inference_state["backbone_out"][ + "language_features" + ], + "language_mask": inference_state["backbone_out"]["language_mask"], + } + } + # run inference for the current frame + ( + obj_id_to_mask, + obj_id_to_score, + tracker_states_local_new, + tracker_metadata_new, + frame_stats, + _, + ) = self._det_track_one_frame( + frame_idx=frame_idx, + num_frames=inference_state["num_frames"], + reverse=reverse, + input_batch=input_batch, + geometric_prompt=geometric_prompt, + tracker_states_local=tracker_states_local, + tracker_metadata_prev=inference_state["tracker_metadata"], + feature_cache=inference_state["feature_cache"], + orig_vid_height=inference_state["orig_height"], + orig_vid_width=inference_state["orig_width"], + is_image_only=inference_state["is_image_only"], + ) + # update inference state + inference_state["sam2_inference_states"] = tracker_states_local_new + inference_state["tracker_metadata"] = tracker_metadata_new + # use a dummy string in "previous_stages_out" to indicate this frame has outputs + inference_state["previous_stages_out"][frame_idx] = "_THIS_FRAME_HAS_OUTPUTS_" + + if self.rank == 0: + self._cache_frame_outputs(inference_state, frame_idx, obj_id_to_mask) + + out = { + "obj_id_to_mask": obj_id_to_mask, + "obj_id_to_score": obj_id_to_score, # first frame detection score + "obj_id_to_sam2_score": tracker_metadata_new[ + "obj_id_to_sam2_score_frame_wise" + ][frame_idx], + } + # removed_obj_ids is only needed on rank 0 to handle hotstart delay buffer + if self.rank == 0: + rank0_metadata = tracker_metadata_new["rank0_metadata"] + removed_obj_ids = rank0_metadata["removed_obj_ids"] + out["removed_obj_ids"] = removed_obj_ids + out["suppressed_obj_ids"] = rank0_metadata["suppressed_obj_ids"][frame_idx] + out["frame_stats"] = frame_stats + if self.masklet_confirmation_enable: + status = rank0_metadata["masklet_confirmation"]["status"] + is_unconfirmed = status == MaskletConfirmationStatus.UNCONFIRMED.value + out["unconfirmed_obj_ids"] = tracker_metadata_new["obj_ids_all_gpu"][ + is_unconfirmed + ].tolist() + else: + out["unconfirmed_obj_ids"] = [] + + return out + + def _postprocess_output( + self, + inference_state, + out, + removed_obj_ids=None, + suppressed_obj_ids=None, + unconfirmed_obj_ids=None, + ): + obj_id_to_mask = out["obj_id_to_mask"] # low res masks + curr_obj_ids = sorted(obj_id_to_mask.keys()) + H_video, W_video = inference_state["orig_height"], inference_state["orig_width"] + if len(curr_obj_ids) == 0: + out_obj_ids = torch.zeros(0, dtype=torch.int64) + out_probs = torch.zeros(0, dtype=torch.float32) + out_binary_masks = torch.zeros(0, H_video, W_video, dtype=torch.bool) + out_boxes_xywh = torch.zeros(0, 4, dtype=torch.float32) + else: + out_obj_ids = torch.tensor(curr_obj_ids, dtype=torch.int64) + out_probs = torch.tensor( + [out["obj_id_to_score"][obj_id] for obj_id in curr_obj_ids] + ) + out_sam2_probs = torch.tensor( + [ + ( + out["obj_id_to_sam2_score"][obj_id] + if obj_id in out["obj_id_to_sam2_score"] + else 0.0 + ) + for obj_id in curr_obj_ids + ] + ) + out_binary_masks = torch.cat( + [obj_id_to_mask[obj_id] for obj_id in curr_obj_ids], dim=0 + ) + + assert out_binary_masks.dtype == torch.bool + keep = out_binary_masks.any(dim=(1, 2)).cpu() # remove masks with 0 areas + # hide outputs for those object IDs in `obj_ids_to_hide` + obj_ids_to_hide = [] + if suppressed_obj_ids is not None: + obj_ids_to_hide.extend(suppressed_obj_ids) + if removed_obj_ids is not None: + obj_ids_to_hide.extend(removed_obj_ids) + if unconfirmed_obj_ids is not None: + obj_ids_to_hide.extend(unconfirmed_obj_ids) + if len(obj_ids_to_hide) > 0: + obj_ids_to_hide_t = torch.tensor(obj_ids_to_hide, dtype=torch.int64) + keep &= ~torch.isin(out_obj_ids, obj_ids_to_hide_t) + + # slice those valid entries from the original outputs + keep_idx = torch.nonzero(keep, as_tuple=True)[0] + keep_idx_gpu = keep_idx.pin_memory().to( + device=out_binary_masks.device, non_blocking=True + ) + + out_obj_ids = torch.index_select(out_obj_ids, 0, keep_idx) + out_probs = torch.index_select(out_probs, 0, keep_idx) + out_sam2_probs = torch.index_select(out_sam2_probs, 0, keep_idx) + out_binary_masks = torch.index_select(out_binary_masks, 0, keep_idx_gpu) + + if perflib.is_enabled: + out_boxes_xyxy = perf_masks_to_boxes( + out_binary_masks, out_obj_ids.tolist() + ) + else: + out_boxes_xyxy = masks_to_boxes(out_binary_masks) + + out_boxes_xywh = box_xyxy_to_xywh(out_boxes_xyxy) # convert to xywh format + # normalize boxes + out_boxes_xywh[..., 0] /= W_video + out_boxes_xywh[..., 1] /= H_video + out_boxes_xywh[..., 2] /= W_video + out_boxes_xywh[..., 3] /= H_video + + # apply non-overlapping constraints on the existing masklets + if out_binary_masks.shape[0] > 1: + assert len(out_binary_masks) == len(out_sam2_probs) + out_binary_masks = ( + self.tracker._apply_object_wise_non_overlapping_constraints( + out_binary_masks.unsqueeze(1), + out_sam2_probs.unsqueeze(1).to(out_binary_masks.device), + background_value=0, + ).squeeze(1) + ) > 0 + + prod_outputs = {} + if self.running_in_prod: + with torch.profiler.record_function( + "Sam3MultiplexTracking._postprocess_output.prod_outputs" + ): + out_centers = torch.zeros( + out_binary_masks.shape[0], + 2, + dtype=torch.float32, + device=out_binary_masks.device, + ) + + y_coords = torch.arange( + H_video, device=out_binary_masks.device, dtype=torch.float32 + ) + x_coords = torch.arange( + W_video, device=out_binary_masks.device, dtype=torch.float32 + ) + y_grid = y_coords.view(1, H_video, 1) + x_grid = x_coords.view(1, 1, W_video) + with torch.profiler.record_function( + "Sam3MultiplexTracking._postprocess_output.prod_outputs.center" + ): + weighted_y_sum = (out_binary_masks * y_grid).sum(dim=(1, 2)) + weighted_x_sum = (out_binary_masks * x_grid).sum(dim=(1, 2)) + total_mass = out_binary_masks.sum(dim=(1, 2)).clamp_min(1e-6) + center_y = weighted_y_sum / total_mass / H_video + center_x = weighted_x_sum / total_mass / W_video + out_centers[:, 0] = center_x + out_centers[:, 1] = center_y + + with torch.profiler.record_function( + "Sam3MultiplexTracking._postprocess_output.prod_outputs.to_cpu" + ): + prod_outputs["out_centers"] = out_centers.cpu().numpy() + + outputs = { + "out_obj_ids": out_obj_ids.cpu().numpy(), + "out_probs": out_probs.cpu().numpy(), + "out_boxes_xywh": out_boxes_xywh.cpu().numpy(), + "out_binary_masks": out_binary_masks.cpu().numpy(), + "frame_stats": out.get("frame_stats", None), + } | prod_outputs + + return outputs + + def _postprocess_output_batched( + self, + H_video, + W_video, + batched_outs, + ): + """ + Batched version of _postprocess_output that batches GPU computations + (keep filtering, box computation) across frames for efficiency. + + Args: + H_video: Video height + W_video: Video width + batched_outs: List of tuples, each containing: + (out, removed_obj_ids, suppressed_obj_ids, unconfirmed_obj_ids) + where out is the output dict from _run_single_frame_inference + + Returns: + List of output dicts, one per frame in batched_outs + """ + batch_size = len(batched_outs) + if batch_size == 0: + return [] + + # ========== Phase 1: Collect per-frame data ========== + # We'll track: frame_data[i] = (obj_ids, probs, sam2_probs, masks, keep_mask, frame_stats) + # or None if frame has no objects + frame_data = [] + device = None + + for ( + out, + removed_obj_ids, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) in batched_outs: + obj_id_to_mask = out["obj_id_to_mask"] + curr_obj_ids = sorted(obj_id_to_mask.keys()) + frame_stats = out.get("frame_stats", None) + + if len(curr_obj_ids) == 0: + frame_data.append((None, None, None, None, None, frame_stats)) + continue + + out_obj_ids = torch.tensor(curr_obj_ids, dtype=torch.int64) + obj_id_to_score_dict = out["obj_id_to_score"] + obj_id_to_sam2_score = out["obj_id_to_sam2_score"] + + if device is None: + device = obj_id_to_mask[curr_obj_ids[0]].device + default_sam2_score = torch.zeros((), dtype=torch.float32, device=device) + + probs_list = [] + sam2_probs_list = [] + binary_masks_list = [] + + for obj_id in curr_obj_ids: + probs_list.append(obj_id_to_score_dict[obj_id]) + sam2_probs_list.append( + obj_id_to_sam2_score.get(obj_id, default_sam2_score) + ) + binary_masks_list.append(obj_id_to_mask[obj_id]) + + out_probs = torch.tensor(probs_list, dtype=torch.float32) + out_sam2_probs_gpu = torch.stack(sam2_probs_list) + out_binary_masks = torch.cat(binary_masks_list, dim=0) + + # Compute keep mask (which objects to hide) + obj_ids_to_hide = [] + if suppressed_obj_ids is not None: + obj_ids_to_hide.extend(suppressed_obj_ids) + if removed_obj_ids is not None: + obj_ids_to_hide.extend(removed_obj_ids) + if unconfirmed_obj_ids is not None: + obj_ids_to_hide.extend(unconfirmed_obj_ids) + + if len(obj_ids_to_hide) > 0: + obj_ids_to_hide_t = torch.tensor(obj_ids_to_hide, dtype=torch.int64) + hide_mask = torch.isin(out_obj_ids, obj_ids_to_hide_t) + else: + hide_mask = torch.zeros(len(out_obj_ids), dtype=torch.bool) + + frame_data.append( + ( + out_obj_ids, + out_probs, + out_sam2_probs_gpu, + out_binary_masks, + hide_mask, + frame_stats, + ) + ) + + # ========== Phase 2: Batch concatenate masks for GPU operations ========== + # Collect frames with objects + frames_with_objects = [] + frame_obj_counts = [] # Number of objects per frame (for frames with objects only) + all_masks_list = [] + all_hide_masks_list = [] + + for i, data in enumerate(frame_data): + if data[0] is not None: + frames_with_objects.append(i) + frame_obj_counts.append(data[0].shape[0]) + all_masks_list.append(data[3]) # binary_masks + all_hide_masks_list.append(data[4]) # hide_mask + + # Handle case where all frames have 0 objects + if len(frames_with_objects) == 0: + outputs = [] + for data in frame_data: + output_dict = { + "out_obj_ids": np.zeros(0, dtype=np.int64), + "out_probs": np.zeros(0, dtype=np.float32), + "out_boxes_xywh": np.zeros((0, 4), dtype=np.float32), + "out_binary_masks": np.zeros((0, H_video, W_video), dtype=bool), + "frame_stats": data[5], + } + if self.running_in_prod: + output_dict["out_centers"] = np.zeros((0, 2), dtype=np.float32) + outputs.append(output_dict) + return outputs + + # Concatenate all masks for batched GPU operations + all_masks = torch.cat(all_masks_list, dim=0) + all_hide_masks = torch.cat(all_hide_masks_list, dim=0) + + # ========== Phase 3: Batched keep mask computation on GPU ========== + # Compute which masks have non-zero area (batched on GPU) + has_area = all_masks.any(dim=(1, 2)) # GPU operation + + # Combine with hide mask (move hide_mask to GPU for the operation) + all_hide_masks_gpu = all_hide_masks.to(device=all_masks.device) + keep_mask_gpu = has_area & ~all_hide_masks_gpu + + # Get keep indices + keep_indices = torch.nonzero(keep_mask_gpu, as_tuple=True)[0] + + if len(keep_indices) == 0: + # All objects filtered out + outputs = [] + for data in frame_data: + output_dict = { + "out_obj_ids": np.zeros(0, dtype=np.int64), + "out_probs": np.zeros(0, dtype=np.float32), + "out_boxes_xywh": np.zeros((0, 4), dtype=np.float32), + "out_binary_masks": np.zeros((0, H_video, W_video), dtype=bool), + "frame_stats": data[5], + } + if self.running_in_prod: + output_dict["out_centers"] = np.zeros((0, 2), dtype=np.float32) + outputs.append(output_dict) + return outputs + + # ========== Phase 4: Batched filtering and box computation ========== + # Filter masks on GPU + kept_masks = torch.index_select(all_masks, 0, keep_indices) + + # Compute bounding boxes in batch on GPU + if perflib.is_enabled: + # Need to gather obj_ids for perflib + all_obj_ids_list = [frame_data[i][0] for i in frames_with_objects] + all_obj_ids_cat = torch.cat(all_obj_ids_list, dim=0) + kept_obj_ids_for_perf = torch.index_select( + all_obj_ids_cat, 0, keep_indices.cpu() + ) + kept_boxes_xyxy = perf_masks_to_boxes( + kept_masks, kept_obj_ids_for_perf.tolist() + ) + else: + kept_boxes_xyxy = masks_to_boxes(kept_masks) + + kept_boxes_xywh = box_xyxy_to_xywh(kept_boxes_xyxy) + kept_boxes_xywh[..., 0] /= W_video + kept_boxes_xywh[..., 1] /= H_video + kept_boxes_xywh[..., 2] /= W_video + kept_boxes_xywh[..., 3] /= H_video + + # ========== Phase 5: Split back to per-frame for non-overlapping ========== + # Compute how many objects were kept per frame + keep_indices_cpu = keep_indices.cpu() + keep_set = set(keep_indices_cpu.tolist()) + + kept_counts = [] + offset = 0 + for count in frame_obj_counts: + kept_in_frame = sum( + 1 for j in range(offset, offset + count) if j in keep_set + ) + kept_counts.append(kept_in_frame) + offset += count + + # Split the kept tensors back to per-frame + split_masks = torch.split(kept_masks, kept_counts) + split_boxes = torch.split(kept_boxes_xywh, kept_counts) + + # Also need to split obj_ids, probs, sam2_probs (filtering from original frame_data) + # We need to track which original indices were kept per frame + frame_kept_indices = [] # List of (local_kept_indices) per frame + offset = 0 + for count in frame_obj_counts: + local_kept = [] + for j in range(offset, offset + count): + if j in keep_set: + local_kept.append(j - offset) # Local index within frame + frame_kept_indices.append(local_kept) + offset += count + + # ========== Phase 6: Apply non-overlapping per frame, collect final results ========== + final_results = [] # List of (frame_idx, obj_ids, probs, boxes, masks) + + for idx, frame_i in enumerate(frames_with_objects): + data = frame_data[frame_i] + local_kept = frame_kept_indices[idx] + + if len(local_kept) == 0: + continue + + # Get the filtered data for this frame + local_kept_t = torch.tensor(local_kept, dtype=torch.int64) + out_obj_ids = torch.index_select(data[0], 0, local_kept_t) + out_probs = torch.index_select(data[1], 0, local_kept_t) + out_sam2_probs = torch.index_select( + data[2], 0, local_kept_t.to(data[2].device) + ) + out_masks = split_masks[idx] + out_boxes = split_boxes[idx] + + # Apply non-overlapping constraints (per-frame operation) + if out_masks.shape[0] > 1: + # Copy sam2_probs to CPU pinned memory then back to GPU for the operation + out_sam2_probs_cpu = torch.empty( + out_sam2_probs.shape, dtype=out_sam2_probs.dtype, pin_memory=True + ) + out_sam2_probs_cpu.copy_(out_sam2_probs, non_blocking=True) + out_masks = ( + self.tracker._apply_object_wise_non_overlapping_constraints( + out_masks.unsqueeze(1), + out_sam2_probs_cpu.unsqueeze(1).to(out_masks.device), + background_value=0, + ).squeeze(1) + ) > 0 + + final_results.append( + (frame_i, out_obj_ids, out_probs, out_boxes, out_masks) + ) + + # ========== Phase 6.5: Compute centers for prod ========== + all_centers = None + if self.running_in_prod and len(final_results) > 0: + with torch.profiler.record_function( + "Sam3MultiplexTracking._postprocess_output_batched.prod_outputs" + ): + # Concatenate all masks for batched center computation + all_masks = torch.cat([r[4] for r in final_results], dim=0) + if all_masks.shape[0] > 0: + y_coords = torch.arange( + H_video, device=all_masks.device, dtype=torch.float32 + ) + x_coords = torch.arange( + W_video, device=all_masks.device, dtype=torch.float32 + ) + y_grid = y_coords.view(1, H_video, 1) + x_grid = x_coords.view(1, 1, W_video) + + weighted_y_sum = (all_masks * y_grid).sum(dim=(1, 2)) + weighted_x_sum = (all_masks * x_grid).sum(dim=(1, 2)) + total_mass = all_masks.sum(dim=(1, 2)).clamp_min(1e-6) + center_y = weighted_y_sum / total_mass / H_video + center_x = weighted_x_sum / total_mass / W_video + all_centers = torch.stack([center_x, center_y], dim=1) + + # Handle case where all filtered out + if len(final_results) == 0: + outputs = [] + for data in frame_data: + output_dict = { + "out_obj_ids": np.zeros(0, dtype=np.int64), + "out_probs": np.zeros(0, dtype=np.float32), + "out_boxes_xywh": np.zeros((0, 4), dtype=np.float32), + "out_binary_masks": np.zeros((0, H_video, W_video), dtype=bool), + "frame_stats": data[5], + } + if self.running_in_prod: + output_dict["out_centers"] = np.zeros((0, 2), dtype=np.float32) + outputs.append(output_dict) + return outputs + + # ========== Phase 7: Concatenate for batched GPU→CPU copy ========== + final_obj_ids = torch.cat([r[1] for r in final_results], dim=0) + final_probs = torch.cat([r[2] for r in final_results], dim=0) + final_boxes = torch.cat([r[3] for r in final_results], dim=0) + final_masks = torch.cat([r[4] for r in final_results], dim=0) + + total_objects = final_obj_ids.shape[0] + + # Initialize or resize batched CPU buffer + batched_buffer_size = self.postprocess_batch_size * self.max_num_objects + needs_buffer_init = not hasattr(self, "buffer_cpu_batched") + needs_buffer_resize = not needs_buffer_init and ( + self.buffer_cpu_batched["out_binary_masks"].shape[0] != batched_buffer_size + or self.buffer_cpu_batched["out_binary_masks"].shape[1] != H_video + or self.buffer_cpu_batched["out_binary_masks"].shape[2] != W_video + ) + + if needs_buffer_init or needs_buffer_resize: + self.buffer_cpu_batched = { + "out_obj_ids": torch.zeros( + batched_buffer_size, + dtype=torch.int64, + device="cpu", + pin_memory=True, + ), + "out_probs": torch.zeros( + batched_buffer_size, + dtype=torch.float32, + device="cpu", + pin_memory=True, + ), + "out_boxes_xywh": torch.zeros( + batched_buffer_size, + 4, + dtype=torch.float32, + device="cpu", + pin_memory=True, + ), + "out_binary_masks": torch.zeros( + batched_buffer_size, + H_video, + W_video, + dtype=bool, + device="cpu", + pin_memory=True, + ), + } + if self.running_in_prod: + self.buffer_cpu_batched["out_centers"] = torch.zeros( + batched_buffer_size, + 2, + dtype=torch.float32, + device="cpu", + pin_memory=True, + ) + + self.buffer_cpu_batched["out_obj_ids"][:total_objects].copy_(final_obj_ids) + self.buffer_cpu_batched["out_probs"][:total_objects].copy_(final_probs) + self.buffer_cpu_batched["out_boxes_xywh"][:total_objects].copy_(final_boxes) + self.buffer_cpu_batched["out_binary_masks"][:total_objects].copy_(final_masks) + + if all_centers is not None: + self.buffer_cpu_batched["out_centers"][:total_objects].copy_(all_centers) + + # ========== Phase 8: Build output list ========== + # Create mapping from frame index to (offset, count) in the buffer + frame_to_offset_count = {} + offset = 0 + for frame_i, obj_ids, _, _, _ in final_results: + count = obj_ids.shape[0] + frame_to_offset_count[frame_i] = (offset, count) + offset += count + + outputs = [] + for i, data in enumerate(frame_data): + frame_stats = data[5] + if i not in frame_to_offset_count: + # Frame has no objects (either originally or after filtering) + output_dict = { + "out_obj_ids": np.zeros(0, dtype=np.int64), + "out_probs": np.zeros(0, dtype=np.float32), + "out_boxes_xywh": np.zeros((0, 4), dtype=np.float32), + "out_binary_masks": np.zeros((0, H_video, W_video), dtype=bool), + "frame_stats": frame_stats, + } + if all_centers is not None: + output_dict["out_centers"] = np.zeros((0, 2), dtype=np.float32) + outputs.append(output_dict) + else: + buf_offset, num_objects = frame_to_offset_count[i] + output_dict = { + "out_obj_ids": self.buffer_cpu_batched["out_obj_ids"][ + buf_offset : buf_offset + num_objects + ] + .numpy() + .copy(), + "out_probs": self.buffer_cpu_batched["out_probs"][ + buf_offset : buf_offset + num_objects + ] + .numpy() + .copy(), + "out_boxes_xywh": self.buffer_cpu_batched["out_boxes_xywh"][ + buf_offset : buf_offset + num_objects + ] + .numpy() + .copy(), + "out_binary_masks": self.buffer_cpu_batched["out_binary_masks"][ + buf_offset : buf_offset + num_objects + ] + .numpy() + .copy(), + "frame_stats": frame_stats, + } + if all_centers is not None: + output_dict["out_centers"] = ( + self.buffer_cpu_batched["out_centers"][ + buf_offset : buf_offset + num_objects + ] + .numpy() + .copy() + ) + outputs.append(output_dict) + + return outputs + + def _cache_frame_outputs( + self, + inference_state, + frame_idx, + obj_id_to_mask, + suppressed_obj_ids=None, + removed_obj_ids=None, + unconfirmed_obj_ids=None, + ): + if "cached_frame_outputs" not in inference_state: + inference_state["cached_frame_outputs"] = {} + + # Filter out suppressed, removed, and unconfirmed objects from the cache + filtered_obj_id_to_mask = obj_id_to_mask.copy() + + objects_to_exclude = set() + if suppressed_obj_ids is not None: + objects_to_exclude.update(suppressed_obj_ids) + if removed_obj_ids is not None: + objects_to_exclude.update(removed_obj_ids) + if unconfirmed_obj_ids is not None: + objects_to_exclude.update(unconfirmed_obj_ids) + + if objects_to_exclude: + for obj_id in objects_to_exclude: + if obj_id in filtered_obj_id_to_mask: + del filtered_obj_id_to_mask[obj_id] + + inference_state["cached_frame_outputs"][frame_idx] = filtered_obj_id_to_mask + + def _build_sam2_output( + self, inference_state, frame_idx, refined_obj_id_to_mask=None + ): + if not frame_idx in inference_state["cached_frame_outputs"]: + return {} + + cached_outputs = inference_state["cached_frame_outputs"][frame_idx] + obj_id_to_mask = cached_outputs.copy() + + # Update with refined masks if provided + if refined_obj_id_to_mask is not None: + for obj_id, refined_mask in refined_obj_id_to_mask.items(): + assert ( + refined_mask is not None + ), f"Refined mask data must be provided for obj_id {obj_id}" + obj_id_to_mask[obj_id] = refined_mask + + return obj_id_to_mask + + def _compile_model(self): + """Compile the SAM model with torch.compile for speedup.""" + # TODO: compile SAM2 model components + is_compiled = getattr(self, "_model_is_compiled", False) + if is_compiled or not self.compile_model: + return + + import torch._dynamo + + # a larger cache size to hold varying number of shapes for torch.compile + # see https://github.com/pytorch/pytorch/blob/v2.5.1/torch/_dynamo/config.py#L42-L49 + torch._dynamo.config.cache_size_limit = 128 + torch._dynamo.config.accumulated_cache_size_limit = 2048 + torch._dynamo.config.capture_scalar_outputs = True + torch._dynamo.config.suppress_errors = True + + # Compile module components following https://www.internalfb.com/diff/D70935785 + # skip compilation of `_encode_prompt` since it sometimes tiggger SymInt errors + # self._encode_prompt = clone_output_wrapper( + # torch.compile(self._encode_prompt, fullgraph=True, mode="max-autotune") + # ) + + ## Compile SAM3 model components (matching OV: clone_output_wrapper(torch.compile(fn))) + self.detector.backbone.language_backbone.encoder.forward = clone_output_wrapper( + torch.compile( + self.detector.backbone.language_backbone.encoder.forward, + fullgraph=True, + mode="max-autotune", + ) + ) + + self.detector.backbone.vision_backbone.forward = clone_output_wrapper( + torch.compile( + self.detector.backbone.vision_backbone.forward, + fullgraph=True, + mode="max-autotune", + ) + ) + self.detector.transformer.encoder.forward = clone_output_wrapper( + torch.compile( + self.detector.transformer.encoder.forward, + fullgraph=True, + mode="max-autotune", + ) + ) + self.detector.transformer.decoder.forward = clone_output_wrapper( + torch.compile( + self.detector.transformer.decoder.forward, + fullgraph=True, + mode="max-autotune", + dynamic=False, # note: FA decoder uses static shapes + ) + ) + + self.detector.segmentation_head.forward = clone_output_wrapper( + torch.compile( + self.detector.segmentation_head.forward, + fullgraph=True, + mode="max-autotune", + ) + ) + + ## Compile SAM2 model components + self.tracker.maskmem_backbone.forward = compile_wrapper( + self.tracker.maskmem_backbone.forward, + mode="max-autotune", + fullgraph=True, + dynamic=False, + ) + + self.tracker.transformer.encoder.forward = shape_logging_wrapper( + compile_wrapper( + self.tracker.transformer.encoder.forward, + mode="max-autotune-no-cudagraphs", + fullgraph=True, + dynamic=True, + ), + keep_kwargs=["src", "src_pos", "prompt", "prompt_pos"], + ) + + self.tracker.sam_mask_decoder.forward = compile_wrapper( + self.tracker.sam_mask_decoder.forward, + mode="max-autotune", + fullgraph=True, + dynamic=False, # Accuracy regression on True + ) + + sam3.model.sam3_video_base._associate_det_trk_compilable = compile_wrapper( + sam3.model.sam3_video_base._associate_det_trk_compilable, + mode="max-autotune-no-cudagraphs", + fullgraph=True, + dynamic=False, + ) + + self.tracker._suppress_object_pw_area_shrinkage = compile_wrapper( + self.tracker._suppress_object_pw_area_shrinkage, + mode="max-autotune-no-cudagraphs", + fullgraph=True, + dynamic=False, + ) + + self._model_is_compiled = True + + def _warm_up_vg_propagation(self, inference_state, start_frame_idx=0): + # use different tracking score thresholds for each round to simulate different number of output objects + num_objects_list = range(self.num_obj_for_compile + 1) + num_rounds = 3 + orig_new_det_thresh = self.new_det_thresh + for i in range(num_rounds): + for num_objects in num_objects_list: + logger.info( + f"round {i + 1}/{num_rounds} warming up model compilation -- simulating {num_objects}/{self.num_obj_for_compile} objects" + ) + # Initialize text prompt and cache image features + self.add_prompt( + inference_state, frame_idx=start_frame_idx, text_str="cat" + ) + if num_objects > 0: + inference_state = self.add_fake_objects_to_inference_state( + inference_state, num_objects, frame_idx=start_frame_idx + ) + inference_state["tracker_metadata"]["rank0_metadata"].update( + { + "masklet_confirmation": { + "status": np.zeros(num_objects, dtype=np.int64), + "consecutive_det_num": np.zeros( + num_objects, dtype=np.int64 + ), + } + } + ) + for _ in self.propagate_in_video( + inference_state, start_frame_idx, reverse=False + ): + pass + for _ in self.propagate_in_video( + inference_state, start_frame_idx, reverse=True + ): + pass + self.reset_state(inference_state) + logger.info( + f"{i + 1}/{num_rounds} warming up model compilation -- completed round {i + 1} out of {num_rounds}" + ) + + # Warm up SAM2 memory encoder with varying input shapes + num_iters = 3 + feat_size = self.tracker.sam_image_embedding_size**2 # 72 * 72 = 5184 + hidden_dim = self.tracker.hidden_dim # 256 + mem_dim = self.tracker.mem_dim # 64 for non-multiplex, 256 for multiplex + is_multiplex = self.tracker.is_multiplex + + for _ in tqdm(range(num_iters)): + for b in range(1, self.num_obj_for_compile + 1): + for i in range( + 1, + self.tracker.max_cond_frames_in_attn + self.tracker.num_maskmem, + ): + for j in range( + self.tracker.max_cond_frames_in_attn + + self.tracker.max_obj_ptrs_in_encoder + ): + if is_multiplex: + # Multiplex encoder: mem_dim == hidden_dim, uses decoupled cross-attention + # num_obj_ptr_tokens = j (since hidden_dim // mem_dim = 1) + num_obj_ptr_tokens = j + memory_seq_len = feat_size * i + num_obj_ptr_tokens + + # src and memory have batch=num_buckets (b) + src = torch.randn( + feat_size, b, hidden_dim, device=self.device + ) + src_pos = torch.randn( + feat_size, b, hidden_dim, device=self.device + ) + memory = torch.randn( + memory_seq_len, b, hidden_dim, device=self.device + ) + memory_pos = torch.randn( + memory_seq_len, b, hidden_dim, device=self.device + ) + + # image and memory_image always have batch=1 (shared image features) + image = torch.randn( + feat_size, 1, hidden_dim, device=self.device + ) + image_pos = torch.randn( + feat_size, 1, hidden_dim, device=self.device + ) + memory_image = torch.randn( + feat_size * i, 1, hidden_dim, device=self.device + ) + memory_image_pos = torch.randn( + feat_size * i, 1, hidden_dim, device=self.device + ) + + self.tracker.transformer.encoder.forward( + image=image, + src=src, + memory_image=memory_image, + memory=memory, + image_pos=image_pos, + src_pos=src_pos, + memory_image_pos=memory_image_pos, + memory_pos=memory_pos, + num_obj_ptr_tokens=num_obj_ptr_tokens, + ) + else: + # Non-multiplex encoder: mem_dim = 64, uses standard cross-attention + # num_obj_ptr_tokens = (hidden_dim // mem_dim) * j = 4 * j + num_obj_ptr_tokens = (hidden_dim // mem_dim) * j + src = torch.randn( + feat_size, b, hidden_dim, device=self.device + ) + src_pos = torch.randn( + feat_size, b, hidden_dim, device=self.device + ) + prompt = torch.randn( + feat_size * i + num_obj_ptr_tokens, + b, + mem_dim, + device=self.device, + ) + prompt_pos = torch.randn( + feat_size * i + num_obj_ptr_tokens, + b, + mem_dim, + device=self.device, + ) + + self.tracker.transformer.encoder.forward( + src=src, + src_pos=src_pos, + prompt=prompt, + prompt_pos=prompt_pos, + num_obj_ptr_tokens=num_obj_ptr_tokens, + ) + + # Warm up different number of kbox + for _ in tqdm(range(num_iters)): + for i in range(1, self.max_num_kboxes + 1): + kboxes = ( + torch.rand(i, 4, dtype=torch.float32) * 0.5 + ) # Generate positive values between 0 and 1 + print( + "Warming up masks_to_boxes with", + i, + f"kboxes.shape={kboxes.shape}", + ) + self.add_prompt( + inference_state, + frame_idx=start_frame_idx, + text_str="cat", + boxes_xywh=kboxes, + box_labels=[1] * len(kboxes), + ) + + for _ in self.propagate_in_video( + inference_state, start_frame_idx, reverse=False + ): + pass + + self.new_det_thresh = orig_new_det_thresh + return inference_state + + def add_fake_objects_to_inference_state( + self, inference_state, num_objects, frame_idx + ): + new_det_obj_ids_local = np.arange(num_objects) + high_res_H, high_res_W = ( + self.tracker.maskmem_backbone.mask_downsampler.interpol_size + ) + new_det_masks = torch.ones( + len(new_det_obj_ids_local), high_res_H, high_res_W + ).to(self.device) + + inference_state["sam2_inference_states"] = self._tracker_add_new_objects( + frame_idx=frame_idx, + num_frames=inference_state["num_frames"], + new_obj_ids=new_det_obj_ids_local, + new_obj_masks=new_det_masks, + tracker_states_local=inference_state["sam2_inference_states"], + orig_vid_height=inference_state["orig_height"], + orig_vid_width=inference_state["orig_width"], + feature_cache=inference_state["feature_cache"], + ) + + # Synthesize obj_id_to_mask data for cached_frame_outputs to support _build_sam2_output during warmup + obj_id_to_mask = {} + if num_objects > 0: + H_video = inference_state["orig_height"] + W_video = inference_state["orig_width"] + + video_res_masks = F.interpolate( + new_det_masks.unsqueeze(1), # Add channel dimension for interpolation + size=(H_video, W_video), + mode="bilinear", + align_corners=False, + ) # (num_objects, 1, H_video, W_video) + for i, obj_id in enumerate(new_det_obj_ids_local): + obj_id_to_mask[obj_id] = (video_res_masks[i] > 0.0).to(torch.bool) + if self.rank == 0: + for fidx in range(inference_state["num_frames"]): + self._cache_frame_outputs(inference_state, fidx, obj_id_to_mask) + + inference_state["tracker_metadata"] = { + "obj_ids_per_gpu": [np.arange(num_objects)], + "obj_ids_all_gpu": np.arange(num_objects), # Same as 1 GPU + "num_obj_per_gpu": [num_objects], + "obj_id_to_score": {i: 1.0 for i in range(num_objects)}, + "obj_id_to_sam2_score_frame_wise": defaultdict(dict), + "obj_id_to_last_occluded": {}, + "max_obj_id": num_objects, + "rank0_metadata": { + "masklet_confirmation": { + "status": np.zeros(num_objects, dtype=np.int64), + "consecutive_det_num": np.zeros(num_objects, dtype=np.int64), + }, + "removed_obj_ids": set(), + "suppressed_obj_ids": defaultdict(set), + }, + # gpu_metadata for hotstart tracking on GPU + "gpu_metadata": { + "N_obj": num_objects, + "obj_first_frame": torch.zeros( + num_objects, dtype=torch.long, device=self.device + ), + "consecutive_unmatch_count": torch.zeros( + num_objects, dtype=torch.long, device=self.device + ), + "trk_keep_alive": torch.ones( + num_objects, dtype=torch.bool, device=self.device + ), + "removed_mask": torch.zeros( + num_objects, dtype=torch.bool, device=self.device + ), + "overlap_pair_counts": torch.zeros( + (num_objects, num_objects), dtype=torch.long, device=self.device + ), + "last_occluded_tensor": torch.zeros( + num_objects, dtype=torch.long, device=self.device + ), + }, + } + # Add num_buc_per_gpu for multiplex mode + if self.is_multiplex: + # Count actual buckets from the inference states + num_buc = self._count_buckets_in_states( + inference_state["sam2_inference_states"] + ) + inference_state["tracker_metadata"]["num_buc_per_gpu"] = np.array( + [num_buc], dtype=np.int64 + ) + + return inference_state + + @torch.inference_mode() + @torch.autocast(device_type="cuda", dtype=torch.bfloat16) + def warm_up_compilation(self): + """ + Warm up the model by running a dummy inference to compile the model. This is + useful to avoid the compilation overhead in the first inference call. + """ + if not self.compile_model: + return + self._warm_up_complete = False + if self.device.type != "cuda": + raise RuntimeError( + f"The model must be on CUDA for warm-up compilation, got {self.device=}." + ) + + # temporally set to single GPU temporarily for warm-up compilation + orig_rank = self.rank + orig_world_size = self.world_size + self.rank = self.detector.rank = 0 + self.world_size = self.detector.world_size = 1 + orig_recondition_every_nth_frame = self.recondition_every_nth_frame + # self.recondition_every_nth_frame = 2 + + # Get a random video + inference_state = self.init_state(resource_path="") + start_frame_idx = 0 + + # Run basic propagation warm-up + inference_state = self._warm_up_vg_propagation(inference_state, start_frame_idx) + + logger.info("Warm-up compilation completed.") + + # revert to the original GPU and rank + self.rank = self.detector.rank = orig_rank + self.world_size = self.detector.world_size = orig_world_size + self.recondition_every_nth_frame = orig_recondition_every_nth_frame + self._warm_up_complete = True + self.tracker.transformer.encoder.forward.set_logging(True) + + @torch.inference_mode() + def add_prompt( + self, + inference_state, + frame_idx, + text_str=None, + clear_old_points=True, + points=None, + point_labels=None, + boxes_xywh=None, + box_labels=None, + clear_old_boxes=True, + output_prob_thresh=0.5, + ): + """ + Add text, point or box prompts on a single frame. This method returns the inference + outputs only on the prompted frame. + + Note that text prompts are NOT associated with a particular frame (i.e. they apply + to all frames). However, we only run inference on the frame specified in `frame_idx`. + + Copied from sam3_demo.Sam3DemoMixin.add_prompt, simplified to support only text prompts. + """ + logger.info("Running add_prompt on frame %d", frame_idx) + + device = inference_state["device"] + num_frames = inference_state["num_frames"] + assert ( + text_str is not None or points is not None or boxes_xywh is not None + ), "at least one type of prompt (text, points, boxes) must be provided" + assert ( + 0 <= frame_idx < num_frames + ), f"{frame_idx=} is out of range for a total of {num_frames} frames" + + assert clear_old_boxes, "clear old boxes must be True" + + assert ( + points is None and clear_old_points is True and point_labels is None + ), "Point prompts not accepted" + + # since it's a semantic prompt, we start over + self.reset_state(inference_state) + + # 1) add text prompt + if text_str is not None: + inference_state["text_prompt"] = text_str + # add the text prompt into the input batch (to be applied to *all* frames) + inference_state["input_batch"].find_text_batch[0] = text_str + for t in range(inference_state["num_frames"]): + text_id = self.TEXT_ID_FOR_TEXT + inference_state["input_batch"].find_inputs[t].text_ids[...] = text_id + + # 2) handle box prompt + assert (boxes_xywh is not None) == (box_labels is not None) + if boxes_xywh is not None: + boxes_xywh = torch.as_tensor(boxes_xywh, dtype=torch.float32) + box_labels = torch.as_tensor(box_labels, dtype=torch.long) + # input boxes are expected to be [xmin, ymin, width, height] format + # in normalized coordinates of range 0~1, similar to FA + assert boxes_xywh.dim() == 2 + assert boxes_xywh.size(0) > 0 and boxes_xywh.size(-1) == 4 + assert box_labels.dim() == 1 and box_labels.size(0) == boxes_xywh.size(0) + boxes_cxcywh = box_xywh_to_cxcywh(boxes_xywh) + assert (boxes_xywh >= 0).all().item() and (boxes_xywh <= 1).all().item() + assert (boxes_cxcywh >= 0).all().item() and (boxes_cxcywh <= 1).all().item() + + new_box_input = boxes_cxcywh, box_labels + inference_state["per_frame_raw_box_input"][frame_idx] = new_box_input + + # handle the case of visual prompt (also added as an input box from the UI) + boxes_cxcywh, box_labels, geometric_prompt = self._get_visual_prompt( + inference_state, frame_idx, boxes_cxcywh, box_labels + ) + + inference_state["per_frame_geometric_prompt"][frame_idx] = geometric_prompt + + with torch.profiler.record_function("add_prompt._init_backbone_out"): + inference_state["backbone_out"] = self._init_backbone_out(inference_state) + out = self._run_single_frame_inference( + inference_state, + frame_idx, + reverse=False, + ) + return frame_idx, self._postprocess_output(inference_state, out) + + def _init_backbone_out(self, inference_state): + """ + Initialize a backbone_out dictionary and extract the text features. + + Note that the visual features of each frame are not extracted here. They will be + extracted on the fly when running inference on each frame. + """ + input = inference_state["input_batch"] + device = self.device + backbone_out = {"img_batch_all_stages": input.img_batch} + text_outputs = self.detector.backbone.forward_text( + input.find_text_batch, device=device + ) + backbone_out.update(text_outputs) + return backbone_out + + @torch.autocast(device_type="cuda", dtype=torch.bfloat16) + def forward(self, input: BatchedDatapoint, is_inference: bool = False): + """This method is only used for benchmark eval (not used in the demo).""" + # set the model to single GPU for benchmark evaluation (to be compatible with trainer) + orig_rank = self.rank + orig_world_size = self.world_size + self.rank = self.detector.rank = 0 + self.world_size = self.detector.world_size = 1 + + # get data + text_prompt_ids = input.find_metadatas[0].original_category_id + text_prompt_list = input.find_text_batch + + # loop over txt prompts + tracking_res = defaultdict(dict) # frame_idx --> {obj_id: mask} + scores_labels = defaultdict(tuple) # obj_id --> (score, text_prompt_id) + inference_state = self.init_state(resource_path=input.raw_images) + for prompt_id, prompt in zip(text_prompt_ids, text_prompt_list): + self.add_prompt(inference_state, frame_idx=0, text_str=prompt) + start_obj_id = max(scores_labels.keys(), default=-1) + 1 # prev max + 1 + + # propagate the prompts + obj_ids_this_prompt = set() + for frame_idx, out in self.propagate_in_video( + inference_state, + start_frame_idx=0, + max_frame_num_to_track=inference_state["num_frames"], + reverse=False, + ): + out_obj_ids = ( + out["out_obj_ids"].numpy() + if isinstance(out["out_obj_ids"], torch.Tensor) + else out["out_obj_ids"] + ) + out_binary_masks = ( + out["out_binary_masks"].numpy() + if isinstance(out["out_binary_masks"], torch.Tensor) + else out["out_binary_masks"] + ) + + current_frame_res = tracking_res[frame_idx] + for obj_id, mask in zip(out_obj_ids, out_binary_masks): + mask_tensor = torch.tensor(mask[None], dtype=torch.bool) + current_frame_res[obj_id + start_obj_id] = mask_tensor + obj_ids_this_prompt.update(current_frame_res.keys()) + + obj_id_to_score = inference_state["tracker_metadata"]["obj_id_to_score"] + for obj_id, score in obj_id_to_score.items(): + if obj_id + start_obj_id in obj_ids_this_prompt: + score_tensor = torch.tensor(score, dtype=torch.float32) + scores_labels[obj_id + start_obj_id] = (score_tensor, prompt_id) + + self.reset_state(inference_state) + + video_id = input.find_metadatas[0].original_image_id[0].cpu().item() + preds = self.prep_for_evaluator(input.raw_images, tracking_res, scores_labels) + + # revert the model to the original GPU and rank + self.rank = self.detector.rank = orig_rank + self.world_size = self.detector.world_size = orig_world_size + return {video_id: preds} + + +class Sam3MultiplexTrackingProd(Sam3MultiplexTracking): + """ + Subclass of Sam3MultiplexTracking with support for batched processing. + + This class enables processing videos in batches rather than all at once by: + 1. Adding an `is_last_batch` parameter to control buffer flushing + 2. Persisting generator state (hotstart_buffer, hotstart_removed_obj_ids, + unconfirmed_obj_ids_per_frame) in inference_state across generator instantiations + + This is useful for processing large videos in smaller chunks to manage memory + or distribute processing across multiple calls. + """ + + @torch.inference_mode() + def init_state( + self, + resource_path, + offload_video_to_cpu=False, + async_loading_frames=False, + use_torchcodec=False, + use_cv2=False, + input_is_mp4=False, + ): + inference_state = super().init_state( + resource_path=resource_path, + offload_video_to_cpu=offload_video_to_cpu, + async_loading_frames=async_loading_frames, + use_torchcodec=use_torchcodec, + use_cv2=use_cv2, + input_is_mp4=input_is_mp4, + ) + # Initialize generator state for batched processing + inference_state["generator_state"] = { + "hotstart_buffer": [], + "hotstart_removed_obj_ids": set(), + "unconfirmed_obj_ids_per_frame": {}, + "postprocess_yield_list": [], + } + return inference_state + + def reset_state(self, inference_state): + super().reset_state(inference_state) + # Reset generator state for batched processing + inference_state["generator_state"] = { + "hotstart_buffer": [], + "hotstart_removed_obj_ids": set(), + "unconfirmed_obj_ids_per_frame": {}, + "postprocess_yield_list": [], + } + + @torch.inference_mode() + def propagate_in_video( + self, + inference_state, + start_frame_idx=None, + max_frame_num_to_track=None, + reverse=False, + output_prob_thresh=0.5, + compute_stability_score=False, + is_instance_processing=False, + is_last_batch=True, + ): + """ + Propagate the prompts to get grounding results for the entire video. This method + is a generator and yields inference outputs for all frames in the range specified + by `start_frame_idx`, `max_frame_num_to_track`, and `reverse`. + + Args: + is_last_batch: Whether this is the last batch in a batched processing scenario. + When True (default), the hotstart buffer will be flushed at end_frame_idx. + When False, the buffer is preserved in inference_state for the next batch. + This flag should be set to False for all batches except the last one when + processing a video in multiple batches. + """ + # compile the model (it's a no-op if the model is already compiled) + # note that it's intentionally added to `self.propagate_in_video`, so that the first + # `self.add_prompt` call will be done in eager mode to fill in the decoder buffers + # such as positional encoding cache) + self._compile_model() + + processing_order, end_frame_idx = self._get_processing_order( + inference_state, + start_frame_idx, + max_frame_num_to_track, + reverse=reverse, + ) + + # Store max_frame_num_to_track in feature_cache for downstream methods + inference_state["feature_cache"]["tracking_bounds"] = { + "max_frame_num_to_track": max_frame_num_to_track, + "propagate_in_video_start_frame_idx": start_frame_idx, + } + + # Initialize or retrieve generator state from inference_state to persist across batches + if "generator_state" not in inference_state: + inference_state["generator_state"] = { + "hotstart_buffer": [], + "hotstart_removed_obj_ids": set(), + "unconfirmed_obj_ids_per_frame": {}, + "postprocess_yield_list": [], + } + + generator_state = inference_state["generator_state"] + hotstart_buffer = generator_state["hotstart_buffer"] + hotstart_removed_obj_ids = generator_state["hotstart_removed_obj_ids"] + unconfirmed_obj_ids_per_frame = generator_state["unconfirmed_obj_ids_per_frame"] + postprocess_yield_list = generator_state.get("postprocess_yield_list", []) + + # when deciding whether to output a masklet on `yield_frame_idx`, we check whether the object is confirmed + # in a future frame (`unconfirmed_frame_delay` frames after the current frame). For example, if we require + # an object to be detected in 3 consecutive frames to be confirmed, then we look 2 frames in the future -- + # e.g., we output an object on frame 4 only if it becomes confirmed on frame 6. + unconfirmed_status_delay = self.masklet_confirmation_consecutive_det_thresh - 1 + + for frame_idx in tqdm( + processing_order, desc="propagate_in_video", disable=self.rank > 0 + ): + out = self._run_single_frame_inference( + inference_state, + frame_idx, + reverse, + is_instance_processing=is_instance_processing, + ) + + if self.hotstart_delay > 0: + # accumulate the outputs for the first `hotstart_delay` frames + hotstart_buffer.append([frame_idx, out]) + # update the object IDs removed by hotstart so that we don't output them + if self.rank == 0: + hotstart_removed_obj_ids.update(out["removed_obj_ids"]) + unconfirmed_obj_ids = out.get("unconfirmed_obj_ids", None) + if unconfirmed_obj_ids is not None: + unconfirmed_obj_ids_per_frame[frame_idx] = unconfirmed_obj_ids + + if frame_idx == end_frame_idx and is_last_batch: + # we reached the end of propagation -- yield all frames in the buffer + yield_list = hotstart_buffer + hotstart_buffer = [] + elif len(hotstart_buffer) >= self.hotstart_delay: + # we have enough frames -- yield and remove the first (oldest) frame from the buffer + yield_list = hotstart_buffer[:1] + hotstart_buffer = hotstart_buffer[1:] + else: + # not enough frames yet -- skip yielding + yield_list = [] + else: + yield_list = [(frame_idx, out)] # output the current frame + + # Accumulate yield_list into postprocess_yield_list + # Snapshot hotstart_removed_obj_ids at the time of accumulation to preserve + # the correct state for each frame (important: this set is mutated over time) + for yield_frame_idx, yield_out in yield_list: + postprocess_yield_list.append( + (yield_frame_idx, yield_out, set(hotstart_removed_obj_ids)) + ) + + # Process batch when we have enough frames + while len(postprocess_yield_list) >= self.postprocess_batch_size: + batch_to_process = postprocess_yield_list[: self.postprocess_batch_size] + postprocess_yield_list = postprocess_yield_list[ + self.postprocess_batch_size : + ] + + with torch.profiler.record_function( + "Sam3MultiplexTrackingProd.postprocess_output_batched" + ): + if self.rank == 0: + # Prepare batched inputs for postprocessing + H_video, W_video = ( + inference_state["orig_height"], + inference_state["orig_width"], + ) + num_frames = inference_state["num_frames"] + + batched_outs = [] + frame_indices = [] + for ( + yield_frame_idx, + yield_out, + removed_obj_ids_snapshot, + ) in batch_to_process: + suppressed_obj_ids = yield_out["suppressed_obj_ids"] + unconfirmed_status_frame_idx = ( + yield_frame_idx + unconfirmed_status_delay + if not reverse + else yield_frame_idx - unconfirmed_status_delay + ) + unconfirmed_status_frame_idx = max( + 0, min(unconfirmed_status_frame_idx, num_frames - 1) + ) + unconfirmed_obj_ids = unconfirmed_obj_ids_per_frame.get( + unconfirmed_status_frame_idx, None + ) + + batched_outs.append( + ( + yield_out, + removed_obj_ids_snapshot, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) + ) + frame_indices.append(yield_frame_idx) + + # Cache frame outputs + self._cache_frame_outputs( + inference_state, + yield_frame_idx, + yield_out["obj_id_to_mask"], + suppressed_obj_ids=suppressed_obj_ids, + removed_obj_ids=removed_obj_ids_snapshot, + unconfirmed_obj_ids=unconfirmed_obj_ids, + ) + + # Process all frames in batch + if self.postprocess_batch_size > 1: + postprocessed_outs = self._postprocess_output_batched( + H_video, W_video, batched_outs + ) + else: + # Process each frame individually but output together + postprocessed_outs = [] + for ( + yield_out, + removed_obj_ids_snapshot, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) in batched_outs: + postprocessed_out = self._postprocess_output( + inference_state, + yield_out, + removed_obj_ids_snapshot, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) + postprocessed_outs.append(postprocessed_out) + + # Yield results + for yield_frame_idx, postprocessed_out in zip( + frame_indices, postprocessed_outs + ): + yield yield_frame_idx, postprocessed_out + else: + # No output on other GPUs + for yield_frame_idx, _, _ in batch_to_process: + yield yield_frame_idx, DUMMY_OUTPUT + + # Handle remaining frames in hotstart buffer at end of last batch + if is_last_batch and len(hotstart_buffer) > 0: + for yield_frame_idx, yield_out in hotstart_buffer: + postprocess_yield_list.append( + (yield_frame_idx, yield_out, set(hotstart_removed_obj_ids)) + ) + hotstart_buffer = [] + + # Flush any remaining frames in the postprocess buffer (even partial + # batches) so that the caller gets results as soon as possible. This is + # especially important for the first batch where hotstart_delay causes + # only a few frames to exit the hotstart buffer — without this flush + # the client would have to wait for the next batch before receiving any + # output, hurting time-to-first-frame. + if len(postprocess_yield_list) > 0: + with torch.profiler.record_function( + "Sam3MultiplexTrackingProd.postprocess_output_batched" + ): + if self.rank == 0: + H_video, W_video = ( + inference_state["orig_height"], + inference_state["orig_width"], + ) + num_frames = inference_state["num_frames"] + + batched_outs = [] + frame_indices = [] + for ( + yield_frame_idx, + yield_out, + removed_obj_ids_snapshot, + ) in postprocess_yield_list: + suppressed_obj_ids = yield_out["suppressed_obj_ids"] + unconfirmed_status_frame_idx = ( + yield_frame_idx + unconfirmed_status_delay + if not reverse + else yield_frame_idx - unconfirmed_status_delay + ) + unconfirmed_status_frame_idx = max( + 0, min(unconfirmed_status_frame_idx, num_frames - 1) + ) + unconfirmed_obj_ids = unconfirmed_obj_ids_per_frame.get( + unconfirmed_status_frame_idx, None + ) + + batched_outs.append( + ( + yield_out, + removed_obj_ids_snapshot, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) + ) + frame_indices.append(yield_frame_idx) + + self._cache_frame_outputs( + inference_state, + yield_frame_idx, + yield_out["obj_id_to_mask"], + suppressed_obj_ids=suppressed_obj_ids, + removed_obj_ids=removed_obj_ids_snapshot, + unconfirmed_obj_ids=unconfirmed_obj_ids, + ) + + if self.postprocess_batch_size > 1: + postprocessed_outs = self._postprocess_output_batched( + H_video, W_video, batched_outs + ) + else: + # Process each frame individually but output together + postprocessed_outs = [] + for ( + yield_out, + removed_obj_ids_snapshot, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) in batched_outs: + postprocessed_out = self._postprocess_output( + inference_state, + yield_out, + removed_obj_ids_snapshot, + suppressed_obj_ids, + unconfirmed_obj_ids, + ) + postprocessed_outs.append(postprocessed_out) + + for yield_frame_idx, postprocessed_out in zip( + frame_indices, postprocessed_outs + ): + yield yield_frame_idx, postprocessed_out + else: + for yield_frame_idx, _, _ in postprocess_yield_list: + yield yield_frame_idx, DUMMY_OUTPUT + + postprocess_yield_list = [] + + # Store the generator state back to inference_state for persistence across batches + generator_state["postprocess_yield_list"] = postprocess_yield_list + generator_state["hotstart_buffer"] = hotstart_buffer + generator_state["hotstart_removed_obj_ids"] = hotstart_removed_obj_ids + generator_state["unconfirmed_obj_ids_per_frame"] = unconfirmed_obj_ids_per_frame + + if self.is_multiplex: + # log the bucket utilization stats + # bucket utilization rate is total valid objects / total capacity -> represents rooms for improvement + # subscription rate is total valid objects / total number of buckets -> represents speedup + total_valid_objects = 0 + total_num_buckets = 0 + for state in inference_state["sam2_inference_states"]: + assert ( + len(state["obj_ids"]) + == state["multiplex_state"].total_valid_entries + ) + total_valid_objects += len(state["obj_ids"]) + total_num_buckets += state["multiplex_state"].num_buckets + if total_num_buckets > 0: + bucket_utilization_rate = ( + total_valid_objects / (total_num_buckets * self.bucket_capacity) + ) * 100 + subscription_rate = (total_valid_objects / total_num_buckets) * 100 + logger.info( + f"Bucket utilization rate: {bucket_utilization_rate:.2f}%, subscription rate: {subscription_rate:.2f}%" + ) + + +class Sam3MultiplexTrackingWithInteractivity(Sam3MultiplexTracking): + def __init__( + self, + use_prev_mem_frame=False, + use_stateless_refinement=False, + refinement_detector_cond_frame_removal_window=30 * 4, + **kwargs, + ): + """ + use_prev_mem_frame: bool, whether to condition on previous memory frames for adding points + use_stateless_refinement: bool, whether to enable stateless refinement behavior + refinement_detector_cond_frame_removal_window: int, we remove a detector conditioning frame if it + is within this many frames of a user refined frame. Set to a large value (e.g. 10000) to + always remove detector conditioning frames if there is any user refinement in the video. + """ + super().__init__(**kwargs) + self.use_prev_mem_frame = use_prev_mem_frame + self.use_stateless_refinement = use_stateless_refinement + self.refinement_detector_cond_frame_removal_window = ( + refinement_detector_cond_frame_removal_window + ) + + @torch.inference_mode() + def init_state( + self, + resource_path, + offload_video_to_cpu=False, + async_loading_frames=False, + use_torchcodec=False, + use_cv2=False, + input_is_mp4=False, + ): + inference_state = super().init_state( + resource_path=resource_path, + offload_video_to_cpu=offload_video_to_cpu, + async_loading_frames=async_loading_frames, + use_torchcodec=use_torchcodec, + use_cv2=use_cv2, + input_is_mp4=input_is_mp4, + ) + # initialize extra states + inference_state["action_history"] = [] # for logging user actions + if self.tracker.per_obj_inference: + # in per_obj mode only 1 inference state is needed, we init it here. + inference_state["sam2_inference_states"] = [ + self._init_new_sam2_state(inference_state) + ] + return inference_state + + def reset_state(self, inference_state): + super().reset_state(inference_state) + # reset extra states + inference_state["action_history"].clear() + if self.tracker.per_obj_inference: + inference_state["sam2_inference_states"] = [ + self._init_new_sam2_state(inference_state) + ] + + def _init_new_sam2_state(self, inference_state): + return self.tracker.init_state( + cached_features=inference_state["feature_cache"], + video_height=inference_state["orig_height"], + video_width=inference_state["orig_width"], + num_frames=inference_state["num_frames"], + ) + + def cancel_propagation(self, inference_state): + """ + Cancel any ongoing propagation and reset the model state. + """ + logger.info("Cancelling ongoing propagation.") + self.add_action_history( + inference_state, + action_type="propagation_cancel", + obj_ids=None, + frame_idx=None, + ) + + def fetch_and_process_single_frame_results(self, inference_state, frame_idx): + tracker_metadata = inference_state["tracker_metadata"] + obj_id_to_mask = inference_state["cached_frame_outputs"][frame_idx] + # post processing - remove suppressed obj_ids + obj_id_to_score = tracker_metadata["obj_id_to_score"] + suppressed_obj_ids = tracker_metadata["rank0_metadata"]["suppressed_obj_ids"][ + frame_idx + ] + obj_id_to_sam2_score = tracker_metadata["obj_id_to_sam2_score_frame_wise"][ + frame_idx + ] + + out = { + "obj_id_to_mask": obj_id_to_mask, + "obj_id_to_score": obj_id_to_score, + "obj_id_to_sam2_score": obj_id_to_sam2_score, + } + return frame_idx, self._postprocess_output( + inference_state, out, suppressed_obj_ids=suppressed_obj_ids + ) + + @torch.inference_mode() + def propagate_in_video( + self, + inference_state, + start_frame_idx=None, + max_frame_num_to_track=None, + reverse=False, + output_prob_thresh=0.5, + compute_stability_score=False, + is_instance_processing=False, + is_last_batch: bool = False, + ): + # step 1: check which type of propagation to run, should be the same for all GPUs. + propagation_type, obj_ids = self.parse_action_history_for_propagation( + inference_state + ) + self.add_action_history( + inference_state, + action_type=propagation_type, + obj_ids=obj_ids, + frame_idx=start_frame_idx, + ) + + # step 2: run full VG propagation + if propagation_type == "propagation_full": + logger.info(f"Running full VG propagation (reverse={reverse}).") + yield from super().propagate_in_video( + inference_state, + start_frame_idx=start_frame_idx, + max_frame_num_to_track=max_frame_num_to_track, + reverse=reverse, + is_last_batch=is_last_batch, + ) + return + + # step 3: run SAM2 partial propagation or direct fetch existing predictions + assert propagation_type in ["propagation_partial", "propagation_fetch"] + logger.info( + f"Running SAM2 propagation for objects {obj_ids} and merging it with existing VG predictions (reverse={reverse})." + if propagation_type == "propagation_partial" + else f"Fetching existing VG predictions without running any propagation (reverse={reverse})." + ) + processing_order, _end_frame_idx = self._get_processing_order( + inference_state, + start_frame_idx=start_frame_idx, + max_frame_num_to_track=max_frame_num_to_track, + reverse=reverse, + ) + + tracker_metadata = inference_state["tracker_metadata"] + + # if fetch just return from output + if propagation_type == "propagation_fetch": + for frame_idx in tqdm(processing_order): + if self.rank == 0: + frame_idx, out = self.fetch_and_process_single_frame_results( + inference_state, frame_idx + ) + yield frame_idx, out + else: + yield frame_idx, DUMMY_OUTPUT # no output for other GPUs + + return + + # get SAM2 inference states containing selected obj_ids + if propagation_type == "propagation_partial": + # can be empty for GPUs where objects are not in their inference states + tracker_states_local = self._get_sam2_inference_states_by_obj_ids( + inference_state, obj_ids + ) + for sam2_state in tracker_states_local: + self.tracker.propagate_in_video_preflight( + sam2_state, run_mem_encoder=True + ) + + for frame_idx in tqdm(processing_order): + # run SAM2 propagation + if propagation_type == "propagation_partial": + self._prepare_backbone_feats(inference_state, frame_idx, reverse) + obj_ids_local, low_res_masks_local, sam2_scores_local = ( + self._propogate_tracker_one_frame_local_gpu( + tracker_states_local, + frame_idx=frame_idx, + reverse=reverse, + run_mem_encoder=True, + ) + ) + + # broadcast refined object sam2 scores and masks to all GPUs + # handle multiple objects that can be located on different GPUs + refined_obj_data = {} # obj_id -> (score, mask_video_res) + + # Collect data for objects on this GPU + local_obj_data = {} + for obj_id in obj_ids: + obj_rank = self._get_gpu_id_by_obj_id(inference_state, obj_id) + if self.rank == obj_rank and obj_id in obj_ids_local: + refined_obj_idx = obj_ids_local.index(obj_id) + refined_mask_low_res = low_res_masks_local[ + refined_obj_idx + ] # (H_low_res, W_low_res) + refined_score = sam2_scores_local[refined_obj_idx] + + # Keep low resolution for broadcasting to reduce communication cost + local_obj_data[obj_id] = (refined_score, refined_mask_low_res) + + # Broadcast data from each GPU that has refined objects + if self.world_size > 1: + for obj_id in obj_ids: + obj_rank = self._get_gpu_id_by_obj_id(inference_state, obj_id) + if self.rank == obj_rank: + # This GPU has the object, broadcast its data + data_to_broadcast = local_obj_data.get(obj_id, None) + data_list = [data_to_broadcast] + self.broadcast_python_obj_cpu(data_list, src=obj_rank) + if data_to_broadcast is not None: + refined_obj_data[obj_id] = data_to_broadcast + elif self.rank != obj_rank: + # This GPU doesn't have the object, receive data + data_list = [None] + self.broadcast_python_obj_cpu(data_list, src=obj_rank) + if data_list[0] is not None: + refined_obj_data[obj_id] = data_list[0] + else: + # Single GPU case + refined_obj_data = local_obj_data + + # Update SAM2 scores for all refined objects + for obj_id, (refined_score, _) in refined_obj_data.items(): + # After broadcast_python_obj_cpu in multi-GPU, tensors may become numpy scalars + # Ensure it's a GPU tensor for consistency with base class behavior + if not isinstance(refined_score, torch.Tensor): + refined_score = torch.tensor( + refined_score, dtype=torch.float32, device=self.device + ) + tracker_metadata["obj_id_to_sam2_score_frame_wise"][ + frame_idx + ].update({obj_id: refined_score}) + + if self.rank == 0: + # get predictions from SAM2 inference states, it includes the original + # VG predictions and the refined predictions from interactivity. + + # Prepare refined masks dictionary - upscale to video resolution after broadcast + refined_obj_id_to_mask = {} + for obj_id, (_, refined_mask_low_res) in refined_obj_data.items(): + refined_mask_video_res = ( + self._convert_low_res_mask_to_video_res( + refined_mask_low_res, inference_state + ) + ) # (1, H_video, W_video) bool + refined_obj_id_to_mask[obj_id] = refined_mask_video_res + + obj_id_to_mask = self._build_sam2_output( + inference_state, frame_idx, refined_obj_id_to_mask + ) + out = { + "obj_id_to_mask": obj_id_to_mask, + "obj_id_to_score": tracker_metadata["obj_id_to_score"], + "obj_id_to_sam2_score": tracker_metadata[ + "obj_id_to_sam2_score_frame_wise" + ][frame_idx], + } + suppressed_obj_ids = tracker_metadata["rank0_metadata"][ + "suppressed_obj_ids" + ][frame_idx] + self._cache_frame_outputs( + inference_state, + frame_idx, + obj_id_to_mask, + suppressed_obj_ids=suppressed_obj_ids, + ) + suppressed_obj_ids = tracker_metadata["rank0_metadata"][ + "suppressed_obj_ids" + ][frame_idx] + yield ( + frame_idx, + self._postprocess_output( + inference_state, out, suppressed_obj_ids=suppressed_obj_ids + ), + ) + else: + yield frame_idx, DUMMY_OUTPUT # no output for other GPUs + + def add_action_history( + self, inference_state, action_type, frame_idx=None, obj_ids=None + ): + """ + action_history is used to automatically decide what to do during propagation. + action_type: one of ["add", "remove", "refine"] + ["propagation_full", "propagation_partial", "propagation_fetch", "propagation_cancel"] + """ + instance_actions = ["add", "remove", "refine"] + propagation_actions = [ + "propagation_full", + "propagation_partial", + "propagation_fetch", + "propagation_cancel", + ] + assert ( + action_type in instance_actions + propagation_actions + ), f"Invalid action type: {action_type}, must be one of {instance_actions + propagation_actions}" + action = { + "type": action_type, + "frame_idx": frame_idx, + "obj_ids": obj_ids, + } + inference_state["action_history"].append(action) + + def _has_object_been_refined(self, inference_state, obj_id): + if "action_history" not in inference_state: + return False + action_history = inference_state["action_history"] + for action in action_history: + if action["type"] in ["add", "refine"] and action.get("obj_ids"): + if obj_id in action["obj_ids"]: + return True + return False + + def parse_action_history_for_propagation(self, inference_state): + action_history = inference_state["action_history"] + if ( + len(action_history) == 1 + and action_history[0]["type"] == "propagation_cancel" + ): + # only one action and it is cancel, we do full propagation + return "propagation_full", None + elif ( + len(action_history) >= 2 + and action_history[-1]["type"] == "propagation_cancel" + ): + # last action is cancel, we go back to the action before cancel + action_before_cancelation = inference_state["action_history"][-2] + # the action before cancelation can be a propagation_fetch from running both forward + # and backward propagation as in webdemo interface, in that case we go back one more step + if action_before_cancelation["type"] == "propagation_fetch": + action_before_cancelation = inference_state["action_history"][-3] + return action_before_cancelation["type"], action_before_cancelation.get( + "obj_ids", None + ) + return self._parse_action_history_for_propagation( + inference_state["action_history"], inference_state["num_frames"] + ) + + def _parse_action_history_for_propagation(self, action_history, num_frames): + """ + Parse the actions in history before the last propagation and prepare for the next propagation. + We support multiple actions (add/remove/refine) between two propagations. If we had an action + history similar to this ["propagate", "add", "refine", "remove", "add"], the next propagation + would remove the removed object, and also propagate the two added/refined objects. + + Returns: + propagation_type: one of ["propagation_full", "propagation_partial", "propagation_fetch"] + - "propagation_full": run VG propagation for all objects + - "propagation_partial": run SAM2 propagation for selected objects, useful for add/refine actions + - "propagation_fetch": fetch existing VG predictions without running any propagation + - "propagation_cancel": this will be handled in parse_action_history_for_propagation() not this function. + obj_ids: list of object ids to run SAM2 propagation on if propagation_type is "propagation_partial". + + TODO: (Jie) this function works for our current workflows, but may need more tests to ensure it works + correctly with different action histories for future workflows. + """ + if len(action_history) == 0: + # we run propagation for the first time + return "propagation_full", None + + if "propagation" in action_history[-1]["type"]: + if action_history[-1]["type"] in ["propagation_fetch"]: + # last propagation is direct fetch, we fetch existing predictions + return "propagation_fetch", None + elif action_history[-1]["type"] in [ + "propagation_partial", + "propagation_full", + ]: + # we do fetch prediction if we have already run propagation twice or we have run + # propagation once and it is from the first frame or last frame. + if ( + len(action_history) > 1 + and action_history[-2]["type"] + in ["propagation_partial", "propagation_full"] + ) or action_history[-1]["frame_idx"] in [ + 0, + num_frames - 1, + ]: + # we have run both forward and backward partial/full propagation + return "propagation_fetch", None + else: + # we have run partial/full forward or backward propagation once, need run it for the rest of the frames + return action_history[-1]["type"], action_history[-1]["obj_ids"] + + # parse actions since last propagation + obj_ids = [] + for action in action_history[::-1]: + if "propagation" in action["type"]: + # we reached the last propagation action, stop parsing + break + if action["type"] in ["add", "refine"]: + obj_ids.extend(action["obj_ids"]) + # else action["type"] == "remove": noop + obj_ids = list(set(obj_ids)) if len(obj_ids) > 0 else None + propagation_type = ( + "propagation_partial" if obj_ids is not None else "propagation_fetch" + ) + return propagation_type, obj_ids + + def remove_object(self, inference_state, obj_id, frame_idx, is_user_action=False): + """ + We try to remove object from sam2 states on every GPU, it will do nothing + for states without this object. + """ + obj_rank = self._get_gpu_id_by_obj_id(inference_state, obj_id) + if obj_rank is None: + # Object was already removed (e.g., by hotstart heuristics during + # propagation). Log a warning and skip SAM2 state and metadata + # removal, but still record action history and clean up cached outputs. + logger.warning( + f"Object {obj_id} not found in any GPU (already removed). " + f"Skipping SAM2 state and metadata removal." + ) + else: + tracker_states_local = inference_state["sam2_inference_states"] + if self.rank == obj_rank: + self._tracker_remove_objects(tracker_states_local, [obj_id]) + + # update metadata + tracker_metadata = inference_state["tracker_metadata"] + _obj_ids = tracker_metadata["obj_ids_per_gpu"][obj_rank] + tracker_metadata["obj_ids_per_gpu"][obj_rank] = _obj_ids[_obj_ids != obj_id] + tracker_metadata["num_obj_per_gpu"][obj_rank] = len( + tracker_metadata["obj_ids_per_gpu"][obj_rank] + ) + tracker_metadata["obj_ids_all_gpu"] = np.concatenate( + tracker_metadata["obj_ids_per_gpu"] + ) + tracker_metadata["obj_id_to_score"].pop(obj_id, None) + # tracker_metadata["max_obj_id"] # we do not reuse the object id, so we do not update it here + + if is_user_action: + self.add_action_history( + inference_state, action_type="remove", obj_ids=[obj_id] + ) + + # Clean up cached frame outputs to remove references to the deleted object + if "cached_frame_outputs" in inference_state: + for _frame_idx in inference_state["cached_frame_outputs"]: + frame_cache = inference_state["cached_frame_outputs"][_frame_idx] + if obj_id in frame_cache: + del frame_cache[obj_id] + + out = None + if frame_idx is not None and self.rank == 0: + frame_idx, out = self.fetch_and_process_single_frame_results( + inference_state, frame_idx + ) + return frame_idx, out + + def _get_gpu_id_by_obj_id(self, inference_state, obj_id): + """ + Locate GPU ID for a given object. + """ + obj_ids_per_gpu = inference_state["tracker_metadata"]["obj_ids_per_gpu"] + for rank, obj_ids in enumerate(obj_ids_per_gpu): + if obj_id in obj_ids: + return rank + return None # object not found in any GPU + + def _get_sam2_inference_states_by_obj_ids(self, inference_state, obj_ids): + """ + Get the SAM2 inference states that contain the given object ids. + This is used to run partial SAM2 propagation on a single object/bucket. + Possibly multiple or zero states can be returned. + """ + states = [ + state + for state in inference_state["sam2_inference_states"] + if set(obj_ids) & set(state["obj_ids"]) + ] + return states + + def _prepare_backbone_feats(self, inference_state, frame_idx, reverse): + input_batch = inference_state["input_batch"] + feature_cache = inference_state["feature_cache"] + num_frames = inference_state["num_frames"] + geometric_prompt = ( + inference_state["constants"]["empty_geometric_prompt"] + if inference_state["per_frame_geometric_prompt"][frame_idx] is None + else inference_state["per_frame_geometric_prompt"][frame_idx] + ) + _ = self.run_backbone_and_detection( + frame_idx=frame_idx, + num_frames=num_frames, + reverse=reverse, + input_batch=input_batch, + geometric_prompt=geometric_prompt, + feature_cache=feature_cache, + ) + + @torch.inference_mode() + def add_prompt( + self, + inference_state, + frame_idx, + text_str=None, + clear_old_points=True, + points=None, + point_labels=None, + boxes_xywh=None, + box_labels=None, + clear_old_boxes=True, + output_prob_thresh=0.5, + obj_id=None, + rel_coordinates=True, + ): + if points is not None: + # SAM2 instance prompts + assert ( + text_str is None and boxes_xywh is None + ), "When points are provided, text_str and boxes_xywh must be None." + assert ( + obj_id is not None + ), "When points are provided, obj_id must be provided." + return self.add_sam2_new_points( + inference_state, + frame_idx, + obj_id=obj_id, + points=points, + labels=point_labels, + clear_old_points=clear_old_points, + rel_coordinates=rel_coordinates, + use_prev_mem_frame=self.use_prev_mem_frame, + ) + else: + # SAM3 prompts — disable batched grounding for single-frame add_prompt + _orig_batched = self.use_batched_grounding + self.use_batched_grounding = False + try: + return super().add_prompt( + inference_state, + frame_idx, + text_str=text_str, + clear_old_points=clear_old_points, + points=points, + point_labels=point_labels, + boxes_xywh=boxes_xywh, + box_labels=box_labels, + clear_old_boxes=clear_old_boxes, + output_prob_thresh=output_prob_thresh, + ) + finally: + self.use_batched_grounding = _orig_batched + + @torch.inference_mode() + def add_sam2_new_points( + self, + inference_state, + frame_idx, + obj_id, + points, + labels, + clear_old_points, + rel_coordinates=True, + use_prev_mem_frame=False, + ): + """Add a new point prompt to SAM2. Suppporting instance refinement to existing + objects by passing existing obj_id or adding a new object by passing a new obj_id. + use_prev_mem_frame=False to disable cross attention to previous memory frames. + Every GPU returns the same results, and results should contain all masks including + these masks not refined or not added by the current user points. + """ + assert obj_id is not None, "obj_id must be provided to add new points" + tracker_metadata = inference_state["tracker_metadata"] + if tracker_metadata == {}: + # initialize masklet metadata if it's uninitialized (empty dict) + tracker_metadata.update(self._initialize_metadata()) + + obj_rank = self._get_gpu_id_by_obj_id(inference_state, obj_id) + + # prepare feature + self._prepare_backbone_feats(inference_state, frame_idx, reverse=False) + + object_has_been_refined = self._has_object_been_refined(inference_state, obj_id) + if ( + obj_rank is not None + and self.use_stateless_refinement + and not object_has_been_refined + ): + # The first time we start refinement on the object, we remove it. + logger.info( + f"[rank={self.rank}] Removing object {obj_id} before refinement." + ) + self.remove_object(inference_state, obj_id, is_user_action=False) + obj_rank = None + elif obj_rank is not None and not object_has_been_refined: + # Extract the object into its own singleton inference state if it belongs to a batch + if self.rank == obj_rank and not self.tracker.per_obj_inference: + tracker_states = self._get_sam2_inference_states_by_obj_ids( + inference_state, [obj_id] + ) + assert len(tracker_states) == 1 + # Check if this is a batched state (contains multiple objects) + sam2_state = tracker_states[0] + if len(sam2_state["obj_ids"]) > 1: + logger.info( + f"[rank={self.rank}] Extracting object {obj_id} into singleton inference state." + ) + self._extract_object_to_singleton_state( + inference_state, obj_id, obj_rank + ) + + if obj_rank is None: + # new object, we assign it a GPU and create a new inference state if limit allows + num_prev_obj = np.sum(tracker_metadata["num_obj_per_gpu"]) + if num_prev_obj >= self.max_num_objects: + logger.warning( + f"add_sam2_new_points: cannot add a new object as we are already tracking {num_prev_obj=} " + f"masklets (under {self.max_num_objects=})" + ) + return frame_idx, None + + new_det_gpu_ids = self._assign_new_det_to_gpus( + new_det_num=1, + prev_workload_per_gpu=tracker_metadata["num_obj_per_gpu"], + ) + obj_rank = new_det_gpu_ids[0] + + # get sam2 inference state for the new object + if self.rank == obj_rank: + if self.tracker.per_obj_inference: + sam2_state = inference_state["sam2_inference_states"][0] + else: + # for batched inference, we create a new inference state + sam2_state = self._init_new_sam2_state(inference_state) + inference_state["sam2_inference_states"].append(sam2_state) + + # update metadata + tracker_metadata["obj_ids_per_gpu"][obj_rank] = np.concatenate( + [ + tracker_metadata["obj_ids_per_gpu"][obj_rank], + np.array([obj_id], dtype=np.int64), + ] + ) + tracker_metadata["num_obj_per_gpu"][obj_rank] = len( + tracker_metadata["obj_ids_per_gpu"][obj_rank] + ) + tracker_metadata["obj_ids_all_gpu"] = np.concatenate( + tracker_metadata["obj_ids_per_gpu"] + ) + tracker_metadata["max_obj_id"] = max(tracker_metadata["max_obj_id"], obj_id) + + logger.info( + f"[rank={self.rank}] Adding new object with id {obj_id} at frame {frame_idx}." + ) + self.add_action_history( + inference_state, "add", frame_idx=frame_idx, obj_ids=[obj_id] + ) + else: + # existing object, for refinement + if self.rank == obj_rank: + tracker_states = self._get_sam2_inference_states_by_obj_ids( + inference_state, [obj_id] + ) + assert ( + len(tracker_states) == 1 + ), f"[rank={self.rank}] Multiple SAM2 inference states found for the same object id." + sam2_state = tracker_states[0] + + # log + logger.info( + f"[rank={self.rank}] Refining existing object with id {obj_id} at frame {frame_idx}." + ) + self.add_action_history( + inference_state, "refine", frame_idx=frame_idx, obj_ids=[obj_id] + ) + + # assign higher score to added/refined object + tracker_metadata["obj_id_to_score"][obj_id] = 1.0 + tracker_metadata["obj_id_to_sam2_score_frame_wise"][frame_idx][obj_id] = ( + torch.tensor(1.0, dtype=torch.float32, device=self.device) + ) + + if self.rank == 0: + rank0_metadata = tracker_metadata.get("rank0_metadata", {}) + + if "removed_obj_ids" in rank0_metadata: + rank0_metadata["removed_obj_ids"].discard(obj_id) + + if "suppressed_obj_ids" in rank0_metadata: + for frame_id in rank0_metadata["suppressed_obj_ids"]: + rank0_metadata["suppressed_obj_ids"][frame_id].discard(obj_id) + + if "masklet_confirmation" in rank0_metadata: + obj_ids_all_gpu = tracker_metadata["obj_ids_all_gpu"] + obj_indices = np.where(obj_ids_all_gpu == obj_id)[0] + if len(obj_indices) > 0: + obj_idx = obj_indices[0] + if obj_idx < len(rank0_metadata["masklet_confirmation"]["status"]): + rank0_metadata["masklet_confirmation"]["status"][obj_idx] = 1 + rank0_metadata["masklet_confirmation"]["consecutive_det_num"][ + obj_idx + ] = self.masklet_confirmation_consecutive_det_thresh + + if self.rank == obj_rank: + should_fallback_to_original_mask = ( + len(points) == 0 and inference_state["is_image_only"] + ) + if should_fallback_to_original_mask: + mask_input = self._get_mask_input(sam2_state, frame_idx, obj_id) + if mask_input is None or 0 in mask_input.shape: + logger.warning( + f"Cannot retrieve original mask input for obj_id {obj_id} at frame {frame_idx} to fallback." + ) + should_fallback_to_original_mask = False + if should_fallback_to_original_mask: + # When user cancels all points on an image, we recover the original mask + # by re-feeding the detector mask to SAM2. + mask_input = self._get_mask_input(sam2_state, frame_idx, obj_id) + # clear out states related to this object to have a fresh start + self.tracker.clear_all_points_in_frame( + sam2_state, frame_idx, obj_id, need_output=False + ) + frame_idx, obj_ids, low_res_masks, video_res_masks = ( + self.tracker.add_new_mask( + sam2_state, + frame_idx, + obj_id, + mask_input, + ) + ) + else: + frame_idx, obj_ids, low_res_masks, video_res_masks = ( + self.tracker.add_new_points( + inference_state=sam2_state, + frame_idx=frame_idx, + obj_id=obj_id, + points=points, + labels=labels, + clear_old_points=clear_old_points, + rel_coordinates=rel_coordinates, + use_prev_mem_frame=use_prev_mem_frame, + ) + ) + + if video_res_masks is not None and len(video_res_masks) > 0: + video_res_masks = fill_holes_in_mask_scores( + video_res_masks, # shape (N, 1, H_video, W_video) + fill_hole_area=self.fill_hole_area, + sprinkle_removal_area=self.sprinkle_removal_area, + fill_holes=True, + remove_sprinkles=True, + ) + + # TODO: will this cause issue when user switching to refine another object? + # Since the mem encoder has already run for the current input points? + # FIX: Synchronize consolidated_frame_inds with actual point/mask + # inputs before propagate_in_video_preflight. Two issues can cause + # the `all_consolidated_frame_inds == input_frames_inds` assertion + # to fail: + # 1) VG detector conditioning frames in mask_inputs_per_obj without + # corresponding point inputs (stale VG entries). + # 2) Previously consolidated point-input frames (from earlier + # add_points) whose consolidated_frame_inds entries were lost + # during subsequent propagation. + # We fix both by: (a) clearing mask-only inputs, (b) rebuilding + # consolidated_frame_inds from the remaining inputs, excluding + # temp output frames (which preflight will add itself). + + # (a) Clear detector-only mask inputs + for _obj_idx in list(sam2_state["mask_inputs_per_obj"].keys()): + _point_frames = set( + sam2_state["point_inputs_per_obj"].get(_obj_idx, {}).keys() + ) + _mask_only_frames = [ + f + for f in list(sam2_state["mask_inputs_per_obj"][_obj_idx].keys()) + if f not in _point_frames + ] + for f in _mask_only_frames: + sam2_state["mask_inputs_per_obj"][_obj_idx].pop(f, None) + + # (b) Rebuild consolidated_frame_inds from remaining inputs + _input_frames = set() + for _oi in sam2_state["point_inputs_per_obj"]: + _input_frames.update(sam2_state["point_inputs_per_obj"][_oi].keys()) + for _oi in sam2_state["mask_inputs_per_obj"]: + _input_frames.update(sam2_state["mask_inputs_per_obj"][_oi].keys()) + # Exclude temp output frames — preflight will consolidate those + _temp_frames = set() + for _obj_temp in sam2_state["temp_output_dict_per_obj"].values(): + _temp_frames.update(_obj_temp["cond_frame_outputs"].keys()) + _temp_frames.update(_obj_temp["non_cond_frame_outputs"].keys()) + _prev_frames = _input_frames - _temp_frames + _cond = set() + _non_cond = set() + for f in _prev_frames: + if f in sam2_state["output_dict"].get("cond_frame_outputs", {}): + _cond.add(f) + else: + _non_cond.add(f) + sam2_state["consolidated_frame_inds"] = { + "cond_frame_outputs": _cond, + "non_cond_frame_outputs": _non_cond, + } + self.tracker.propagate_in_video_preflight(sam2_state, run_mem_encoder=True) + if not inference_state["is_image_only"]: + # Clear detector conditioning frames when user clicks are received to allow + # model updating masks on these frames. It is a noop if user is refining on the + # detector conditioning frames or adding new objects. + self.clear_detector_added_cond_frame_in_sam2( + sam2_state, obj_id, frame_idx + ) + + # fetch results from states and gather across GPUs + # Use optimized caching approach to avoid reprocessing unmodified objects + if self.rank == obj_rank and len(obj_ids) > 0: + new_mask_data = (video_res_masks[obj_ids.index(obj_id)] > 0.0).to( + torch.bool + ) + else: + new_mask_data = None + + # Broadcast the new mask data across all ranks for consistency + if self.world_size > 1: + data_list = [new_mask_data] + self.broadcast_python_obj_cpu(data_list, src=obj_rank) + new_mask_data = data_list[0] + + if self.rank == 0: + obj_id_to_mask = self._build_sam2_output( + inference_state, + frame_idx, + {obj_id: new_mask_data} if new_mask_data is not None else None, + ) + # post processing - remove suppressed obj_ids + obj_id_to_score = tracker_metadata["obj_id_to_score"] + suppressed_obj_ids = tracker_metadata["rank0_metadata"][ + "suppressed_obj_ids" + ][frame_idx] + obj_id_to_sam2_score = tracker_metadata["obj_id_to_sam2_score_frame_wise"][ + frame_idx + ] + + out = { + "obj_id_to_mask": obj_id_to_mask, + "obj_id_to_score": obj_id_to_score, + "obj_id_to_sam2_score": obj_id_to_sam2_score, + } + self._cache_frame_outputs( + inference_state, + frame_idx, + obj_id_to_mask, + suppressed_obj_ids=suppressed_obj_ids, + ) + return frame_idx, self._postprocess_output( + inference_state, out, suppressed_obj_ids=suppressed_obj_ids + ) + else: + return frame_idx, None # no output on other GPUs + + def _get_mask_input(self, inference_state, frame_idx, obj_id): + """Get the mask input for a specific object on a specific frame.""" + obj_idx = self.tracker._obj_id_to_idx(inference_state, obj_id) + mask_inputs_per_frame = inference_state["mask_inputs_per_obj"][obj_idx] + if frame_idx not in mask_inputs_per_frame: + logger.info( + f"frame {frame_idx} not in mask_inputs_per_frame for obj_id {obj_id}" + ) + return None + + mask_inputs_orig = mask_inputs_per_frame[frame_idx].squeeze(0, 1) # (H, W) + return mask_inputs_orig + + def _gather_obj_id_to_mask_across_gpus(self, inference_state, obj_id_to_mask_local): + """Gather obj_id_to_mask from all GPUs. Optionally resize the masks to the video resolution.""" + tracker_metadata = inference_state["tracker_metadata"] + + # concatenate the output masklets from all local inference states + H_mask = W_mask = self.tracker.low_res_mask_size + obj_ids_local = tracker_metadata["obj_ids_per_gpu"][self.rank] + low_res_masks_local = [] + for obj_id in obj_ids_local: + if obj_id in obj_id_to_mask_local: + low_res_masks_local.append(obj_id_to_mask_local[obj_id]) + else: + low_res_masks_local.append( + torch.full((H_mask, W_mask), -1024.0, device=self.device) + ) + if len(low_res_masks_local) > 0: + low_res_masks_local = torch.stack(low_res_masks_local, dim=0) # (N, H, W) + assert low_res_masks_local.shape[1:] == (H_mask, W_mask) + else: + low_res_masks_local = torch.zeros(0, H_mask, W_mask, device=self.device) + + # all-gather `low_res_masks_local` into `low_res_masks_global` + # - low_res_masks_global: Tensor -- (num_global_obj, H_mask, W_mask) + if self.world_size > 1: + low_res_masks_local = low_res_masks_local.float().contiguous() + low_res_masks_peers = [ + low_res_masks_local.new_empty(num_obj, H_mask, W_mask) + for num_obj in tracker_metadata["num_obj_per_gpu"] + ] + dist.all_gather(low_res_masks_peers, low_res_masks_local) + low_res_masks_global = torch.cat(low_res_masks_peers, dim=0) + else: + low_res_masks_global = low_res_masks_local + return low_res_masks_global + + def _convert_low_res_mask_to_video_res(self, low_res_mask, inference_state): + """ + Convert a low-res mask to video resolution, matching the format expected by _build_sam2_output. + + Args: + low_res_mask: Tensor of shape (H_low_res, W_low_res) + inference_state: Contains video dimensions + + Returns: + video_res_mask: Tensor of shape (1, H_video, W_video) bool + """ + if low_res_mask is None: + return None + + # Convert to 3D for interpolation: (H_low_res, W_low_res) -> (1, H_low_res, W_low_res) + low_res_mask_3d = low_res_mask.unsqueeze(0).unsqueeze(0) + + # Get video dimensions + H_video = inference_state["orig_height"] + W_video = inference_state["orig_width"] + + video_res_mask = F.interpolate( + low_res_mask_3d.float(), + size=(H_video, W_video), + mode="bilinear", + align_corners=False, + ) # (1, H_video, W_video) + + # Convert to boolean - already in the right shape! + return (video_res_mask.squeeze(0) > 0.0).to(torch.bool) + + def clear_detector_added_cond_frame_in_sam2( + self, sam2_state, obj_id, refined_frame_idx + ): + """Clear detector added conditioning frame if it is within a predefined window + of the refined frame. This allow model to update masks on these frames.""" + obj_idx = self.tracker._obj_id_to_idx(sam2_state, obj_id) + + mask_only_cond_frame_indices = [] + window = self.refinement_detector_cond_frame_removal_window + for frame_idx in sam2_state["mask_inputs_per_obj"][obj_idx]: + if frame_idx not in sam2_state["point_inputs_per_obj"][obj_idx]: + # clear conditioning frames within a window of the refined frame + if abs(frame_idx - refined_frame_idx) <= window: + mask_only_cond_frame_indices.append(frame_idx) + + # clear + if len(mask_only_cond_frame_indices) > 0: + for frame_idx in mask_only_cond_frame_indices: + # obj_ids_on_this_frame is essentially all obj_ids in the state + # since they are bucket batched + obj_ids_on_this_frame = sam2_state["obj_id_to_idx"].keys() + for obj_id2 in obj_ids_on_this_frame: + self.tracker.clear_all_points_in_frame( + sam2_state, frame_idx, obj_id2, need_output=False + ) + logger.info( + f"Cleared detector mask only conditioning frames ({mask_only_cond_frame_indices}) in SAM2." + ) + return + + def _extract_object_to_singleton_state(self, inference_state, obj_id, obj_rank): + """ + Extract an object from a batched inference state into its own singleton state. + """ + if self.rank != obj_rank: + return + + tracker_states_local = inference_state["sam2_inference_states"] + + # Find the inference state containing this object + source_state = None + source_state_idx = None + for idx, state in enumerate(tracker_states_local): + if obj_id in state["obj_ids"]: + source_state = state + source_state_idx = idx + break + + assert source_state is not None + + if len(source_state["obj_ids"]) <= 1: + # Object not found or already in singleton state + return + + # Step 1: Extract all the object's state data before removing it + obj_idx_in_source = source_state["obj_id_to_idx"][obj_id] + multiplex_state = source_state.get("multiplex_state") + + # Extract consolidated outputs (obj_ptr, maskmem_features, etc.) BEFORE + # remove_object modifies the source tensors. + singleton_consolidated_outputs = { + "cond_frame_outputs": {}, + "non_cond_frame_outputs": {}, + } + if "output_dict" in source_state: + for storage_key in ["cond_frame_outputs", "non_cond_frame_outputs"]: + source_outputs = source_state["output_dict"].get(storage_key, {}) + for f_idx, source_frame_out in source_outputs.items(): + if source_frame_out["pred_masks"].shape[0] < obj_idx_in_source + 1: + continue + singleton_frame_out = { + "pred_masks": source_frame_out["pred_masks"][ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone(), + "object_score_logits": source_frame_out["object_score_logits"][ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone(), + "image_features": source_frame_out.get("image_features"), + "image_pos_enc": source_frame_out.get("image_pos_enc"), + "local_obj_id_to_idx": {obj_id: 0}, + } + # Extract maskmem_features (demux from multiplex space) + maskmem_features = source_frame_out.get("maskmem_features") + if maskmem_features is not None and multiplex_state is not None: + try: + demuxed = multiplex_state.demux(maskmem_features) + maskmem_features = demuxed[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + except (AssertionError, IndexError): + maskmem_features = None + elif maskmem_features is not None: + maskmem_features = maskmem_features[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + singleton_frame_out["maskmem_features"] = maskmem_features + # Extract maskmem_pos_enc (demux level by level) + maskmem_pos_enc = source_frame_out.get("maskmem_pos_enc") + if maskmem_pos_enc is not None: + remapped = [] + for level_enc in maskmem_pos_enc: + if level_enc is None: + remapped.append(None) + continue + if multiplex_state is not None: + try: + demuxed = multiplex_state.demux(level_enc) + remapped.append( + demuxed[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + ) + except (AssertionError, IndexError): + remapped.append(None) + else: + remapped.append( + level_enc[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + ) + maskmem_pos_enc = remapped + singleton_frame_out["maskmem_pos_enc"] = maskmem_pos_enc + # Extract obj_ptr (demux from multiplex space) + if ( + "obj_ptr" in source_frame_out + and self.tracker.use_obj_ptrs_in_encoder + ): + source_obj_ptr = source_frame_out["obj_ptr"] + if multiplex_state is not None: + obj_ptr_data = multiplex_state.demux(source_obj_ptr) + singleton_frame_out["obj_ptr"] = obj_ptr_data[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + else: + singleton_frame_out["obj_ptr"] = source_obj_ptr[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + # Extract conditioning_objects + if "conditioning_objects" in source_frame_out: + if ( + obj_idx_in_source + in source_frame_out["conditioning_objects"] + ): + singleton_frame_out["conditioning_objects"] = {0} + else: + singleton_frame_out["conditioning_objects"] = set() + singleton_consolidated_outputs[storage_key][f_idx] = ( + singleton_frame_out + ) + + # Extract point and mask inputs for this object + extracted_point_inputs = {} + extracted_mask_inputs = {} + + if ( + "point_inputs_per_obj" in source_state + and obj_idx_in_source in source_state["point_inputs_per_obj"] + ): + extracted_point_inputs = source_state["point_inputs_per_obj"][ + obj_idx_in_source + ].copy() + + if ( + "mask_inputs_per_obj" in source_state + and obj_idx_in_source in source_state["mask_inputs_per_obj"] + ): + extracted_mask_inputs = source_state["mask_inputs_per_obj"][ + obj_idx_in_source + ].copy() + + # Extract per-object outputs - these are already properly sliced for the object + extracted_obj_cond_outputs = {} + extracted_obj_non_cond_outputs = {} + extracted_temp_cond_outputs = {} + extracted_temp_non_cond_outputs = {} + + if ( + "output_dict_per_obj" in source_state + and obj_idx_in_source in source_state["output_dict_per_obj"] + ): + obj_output_dict = source_state["output_dict_per_obj"][obj_idx_in_source] + extracted_obj_cond_outputs = obj_output_dict.get( + "cond_frame_outputs", {} + ).copy() + cond_input_keys = ( + extracted_point_inputs.keys() | extracted_mask_inputs.keys() + ) + # we may have obj cond outputs for other objects in a batch, so limit to cond inputs for only this object + extracted_obj_cond_outputs = { + k: v + for k, v in extracted_obj_cond_outputs.items() + if k in cond_input_keys + } + + extracted_obj_non_cond_outputs = obj_output_dict.get( + "non_cond_frame_outputs", {} + ).copy() + + if ( + "temp_output_dict_per_obj" in source_state + and obj_idx_in_source in source_state["temp_output_dict_per_obj"] + ): + temp_obj_output_dict = source_state["temp_output_dict_per_obj"][ + obj_idx_in_source + ] + extracted_temp_cond_outputs = temp_obj_output_dict.get( + "cond_frame_outputs", {} + ).copy() + extracted_temp_non_cond_outputs = temp_obj_output_dict.get( + "non_cond_frame_outputs", {} + ).copy() + + # Step 2: Remove the object from the source state + remaining_obj_ids, _ = self.tracker.remove_object( + source_state, obj_id, strict=False, need_output=False + ) + + # Step 3: Create a new singleton inference state + new_sam2_state = self.tracker.init_state( + cached_features=inference_state["feature_cache"], + video_height=inference_state["orig_height"], + video_width=inference_state["orig_width"], + num_frames=inference_state["num_frames"], + ) + + # Step 4: Set up the singleton state structure for the extracted object + # Map the object to index 0 in the new singleton state + new_sam2_state["obj_id_to_idx"] = {obj_id: 0} + new_sam2_state["obj_idx_to_id"] = {0: obj_id} + new_sam2_state["obj_ids"] = [obj_id] + + # Step 5: Restore all the extracted state + # Restore point and mask inputs + new_sam2_state["point_inputs_per_obj"] = {0: extracted_point_inputs} + new_sam2_state["mask_inputs_per_obj"] = {0: extracted_mask_inputs} + + # Restore per-object output dictionaries (already properly sliced) + new_sam2_state["output_dict_per_obj"] = { + 0: { + "cond_frame_outputs": extracted_obj_cond_outputs, + "non_cond_frame_outputs": extracted_obj_non_cond_outputs, + } + } + + # Restore temporary outputs + new_sam2_state["temp_output_dict_per_obj"] = { + 0: { + "cond_frame_outputs": extracted_temp_cond_outputs, + "non_cond_frame_outputs": extracted_temp_non_cond_outputs, + } + } + + # Step 6: Rebuild the consolidated output_dict for the singleton state + # Use the extracted consolidated outputs which include obj_ptr, + # maskmem_features, maskmem_pos_enc (not just pred_masks/object_score_logits) + + # Create singleton multiplex state and remux extracted tensors + new_multiplex_state = self.tracker.multiplex_controller.get_state( + num_valid_entries=1, + device=source_state.get("device", "cuda"), + dtype=torch.float32, + random=False, + object_ids=[obj_id], + ) + new_sam2_state["multiplex_state"] = new_multiplex_state + + for storage_key in ["cond_frame_outputs", "non_cond_frame_outputs"]: + for f_idx, frame_out in singleton_consolidated_outputs[storage_key].items(): + if frame_out.get("maskmem_features") is not None: + frame_out["maskmem_features"] = frame_out[ + "maskmem_features" + ].clone() + if frame_out.get("maskmem_pos_enc") is not None: + frame_out["maskmem_pos_enc"] = [ + level.clone() if level is not None else None + for level in frame_out["maskmem_pos_enc"] + ] + if "obj_ptr" in frame_out and self.tracker.use_obj_ptrs_in_encoder: + frame_out["obj_ptr"] = new_multiplex_state.mux(frame_out["obj_ptr"]) + + new_sam2_state["output_dict"] = singleton_consolidated_outputs + + # Step 7: Copy other important state if it exists + for key in [ + "first_ann_frame_idx", + "tracking_has_started", + ]: + if key in source_state: + new_sam2_state[key] = source_state[key] + + # Leave consolidated_frame_inds empty so preflight reconstructs from per-obj data + new_sam2_state["consolidated_frame_inds"] = { + "cond_frame_outputs": set(), + "non_cond_frame_outputs": set(), + } + + # Step 8: Add the new singleton state to the list + tracker_states_local.append(new_sam2_state) + + # Step 9: If the source state is now empty, remove it + if len(remaining_obj_ids) == 0: + tracker_states_local.pop(source_state_idx) + logger.info( + f"Removed empty inference state after extracting object {obj_id}" + ) + + logger.info(f"Object {obj_id} successfully extracted to singleton state") diff --git a/sam3/model/sam3_multiplex_video_predictor.py b/sam3/model/sam3_multiplex_video_predictor.py new file mode 100644 index 0000000..b86c7f1 --- /dev/null +++ b/sam3/model/sam3_multiplex_video_predictor.py @@ -0,0 +1,63 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved + +# pyre-unsafe + +""" +Sam3MultiplexVideoPredictor — user-facing entry point for SAM 3.1 multiplex. + +Ported from onevision Sam3Model (webdemo/ta/models/sam3_model.py). +Handles warm-up compilation, bf16 autocast, and session management +via the shared Sam3BasePredictor handle_request/handle_stream_request API. +""" + +from typing import Dict, Optional + +import torch +from sam3.logger import get_logger +from sam3.model.sam3_base_predictor import Sam3BasePredictor + +logger = get_logger(__name__) + + +class Sam3MultiplexVideoPredictor(Sam3BasePredictor): + """ + User-facing predictor for SAM 3.1 multiplex video tracking. + + Wraps Sam3MultiplexTrackingWithInteractivity with: + - bf16 autocast + - Warm-up compilation (when compile=True) + - Session expiration management + - handle_request / handle_stream_request dispatch API (from Sam3BasePredictor) + """ + + def __init__( + self, + model, + session_expiration_sec=1200, + default_output_prob_thresh=0.5, + async_loading_frames=True, + warm_up=False, + ): + super().__init__() + self.model = model + self.session_expiration_sec = session_expiration_sec + self.default_output_prob_thresh = default_output_prob_thresh + self.async_loading_frames = async_loading_frames + + # turn on tfloat32 for Ampere GPUs + torch.backends.cuda.matmul.allow_tf32 = True + torch.backends.cudnn.allow_tf32 = True + # use bfloat16 inference for Flash Attention kernel + self.bf16_context = torch.autocast(device_type="cuda", dtype=torch.bfloat16) + self.bf16_context.__enter__() + + if warm_up: + self.model._warm_up_complete = False + self.model.warm_up_compilation() + self.model._warm_up_complete = True + + def _extend_expiration_time(self, session): + """Update last-use time and store session expiration timeout.""" + super()._extend_expiration_time(session) + if self.session_expiration_sec: + session["expiration_sec"] = self.session_expiration_sec diff --git a/sam3/model/sam3_tracker_utils.py b/sam3/model/sam3_tracker_utils.py index e971dac..74e9e12 100644 --- a/sam3/model/sam3_tracker_utils.py +++ b/sam3/model/sam3_tracker_utils.py @@ -367,7 +367,17 @@ def get_best_gt_match_from_multimasks(pred_multimasks, gt_masks, pred_scores=Non return best_pred_mask -def fill_holes_in_mask_scores(mask, max_area, fill_holes=True, remove_sprinkles=True): +def fill_holes_in_mask_scores( + mask, + max_area=None, + fill_holes=True, + remove_sprinkles=True, + fill_hole_area=None, + sprinkle_removal_area=None, +): + # Support onevision-style keyword args + if fill_hole_area is not None and max_area is None: + max_area = fill_hole_area """ A post processor to fill small holes in mask scores with area under `max_area`. Holes are those small connected components in either background or foreground. diff --git a/sam3/model/sam3_video_base.py b/sam3/model/sam3_video_base.py index 8780f1a..a77bdd3 100644 --- a/sam3/model/sam3_video_base.py +++ b/sam3/model/sam3_video_base.py @@ -8,8 +8,9 @@ import os from collections import defaultdict from copy import deepcopy +from dataclasses import dataclass from enum import Enum -from typing import Any, Dict, List, Set +from typing import Any, Dict, List, Optional, Set, Tuple import numpy as np import numpy.typing as npt @@ -22,7 +23,7 @@ from sam3.model.data_misc import BatchedDatapoint from sam3.model.sam3_tracker_utils import fill_holes_in_mask_scores, mask_to_box from sam3.perflib.masks_ops import mask_iou -from sam3.train.masks_ops import rle_encode +from sam3.train.masks_ops import mask_iom, rle_encode from torch import nn, Tensor logger = get_logger(__name__) @@ -33,6 +34,226 @@ class MaskletConfirmationStatus(Enum): CONFIRMED = 2 # confirmed by at least one detection +@dataclass +class RealizedAssociateDetTrkresult: + new_det_fa_inds: np.array + unmatched_trk_obj_ids: np.array + det_to_matched_trk_obj_ids: Dict[int, np.array] + trk_id_to_max_iou_high_conf_det: Dict[int, int] + empty_trk_obj_ids: np.array + new_det_obj_ids: Optional[np.array] = None + new_det_gpu_ids: Optional[np.array] = None + num_obj_dropped_due_to_limit: Optional[int] = None + + def get_new_det_gpu_ids( + self, tracker_metadata_prev, is_image_only, det_scores, tracking_obj + ): + with torch.profiler.record_function("get_new_det_gpu_ids"): + if self.new_det_obj_ids is None: + det_scores_np: np.ndarray = det_scores.cpu().numpy() + prev_obj_num = np.sum(tracker_metadata_prev["num_obj_per_gpu"]) + new_det_num = len(self.new_det_fa_inds) + num_obj_dropped_due_to_limit = 0 + if ( + not is_image_only + and prev_obj_num + new_det_num > tracking_obj.max_num_objects + ): + logger.warning( + f"hitting {tracking_obj.max_num_objects=} with {new_det_num=} and {prev_obj_num=}" + ) + new_det_num_to_keep = tracking_obj.max_num_objects - prev_obj_num + num_obj_dropped_due_to_limit = new_det_num - new_det_num_to_keep + self.new_det_fa_inds = tracking_obj._drop_new_det_with_obj_limit( + self.new_det_fa_inds, det_scores_np, new_det_num_to_keep + ) + assert len(self.new_det_fa_inds) == new_det_num_to_keep + new_det_num = len(self.new_det_fa_inds) + new_det_start_obj_id = tracker_metadata_prev["max_obj_id"] + 1 + new_det_obj_ids = new_det_start_obj_id + np.arange(new_det_num) + if tracking_obj.is_multiplex: + prev_workload_per_gpu = tracker_metadata_prev["num_buc_per_gpu"] + else: + prev_workload_per_gpu = tracker_metadata_prev["num_obj_per_gpu"] + new_det_gpu_ids = tracking_obj._assign_new_det_to_gpus( + new_det_num=new_det_num, + prev_workload_per_gpu=prev_workload_per_gpu, + ) + self.new_det_obj_ids = new_det_obj_ids + self.new_det_gpu_ids = new_det_gpu_ids + self.num_obj_dropped_due_to_limit = num_obj_dropped_due_to_limit + return ( + self.new_det_obj_ids, + self.new_det_gpu_ids, + self.num_obj_dropped_due_to_limit, + ) + + +def realize_adt_result(adt_lazy_result, tracker_metadata_prev, det_mask_preds): + if isinstance(adt_lazy_result, LazyAssociateDetTrkResult): + adt_lazy_result._convert_to_numpy() + return adt_lazy_result._create_cpu_metadata( + tracker_metadata_prev["obj_ids_all_gpu"], det_mask_preds + ) + return adt_lazy_result + + +class LazyAssociateDetTrkResult: + def __init__( + self, + trk_is_unmatched: Tensor, + trk_is_nonempty: Tensor, + is_new_det: Tensor, + det_to_max_iou_trk_idx: Tensor, + det_is_high_conf: Tensor, + det_is_high_iou: Tensor, + det_keep: Tensor, + im_mask: Tensor, + ): + self.trk_is_unmatched = trk_is_unmatched + self.trk_is_nonempty = trk_is_nonempty + self.is_new_det = is_new_det + self.det_to_max_iou_trk_idx = det_to_max_iou_trk_idx + self.det_is_high_conf = det_is_high_conf + self.det_is_high_iou = det_is_high_iou + self.det_keep = det_keep + self.im_mask = im_mask + + def _convert_to_numpy(self): + with torch.profiler.record_function("Convert to numpy"): + self.trk_is_unmatched = self.trk_is_unmatched.cpu().numpy() + self.trk_is_nonempty = self.trk_is_nonempty.cpu().numpy() + self.is_new_det = self.is_new_det.cpu().numpy() + self.det_to_max_iou_trk_idx = self.det_to_max_iou_trk_idx.cpu().numpy() + self.det_is_high_conf = self.det_is_high_conf.cpu().numpy() + self.det_is_high_iou = self.det_is_high_iou.cpu().numpy() + self.det_keep = self.det_keep.cpu().numpy().tolist() + self.im_mask = self.im_mask.cpu().numpy() + + def _create_cpu_metadata(self, trk_obj_ids, det_masks): + with torch.profiler.record_function("_create_cpu_metadata"): + unmatched_trk_obj_ids = trk_obj_ids[self.trk_is_unmatched] + empty_trk_obj_ids = trk_obj_ids[~self.trk_is_nonempty] + new_det_fa_inds = np.nonzero(self.is_new_det)[0] + det_is_high_conf_and_iou = set( + np.nonzero(self.det_is_high_conf & self.det_is_high_iou)[0] + ) + det_to_matched_trk_obj_ids = {} + trk_id_to_max_iou_high_conf_det = {} + for d in range(det_masks.size(0)): + if self.det_keep[d]: + det_to_matched_trk_obj_ids[d] = trk_obj_ids[self.im_mask[d, :]] + if d in det_is_high_conf_and_iou: + trk_obj_id = trk_obj_ids[self.det_to_max_iou_trk_idx[d]].item() + trk_id_to_max_iou_high_conf_det[trk_obj_id] = d + return RealizedAssociateDetTrkresult( + new_det_fa_inds=new_det_fa_inds, + unmatched_trk_obj_ids=unmatched_trk_obj_ids, + det_to_matched_trk_obj_ids=det_to_matched_trk_obj_ids, + trk_id_to_max_iou_high_conf_det=trk_id_to_max_iou_high_conf_det, + empty_trk_obj_ids=empty_trk_obj_ids, + ) + + +def _associate_det_trk_compilable( + det_masks, + det_scores, + det_keep, + trk_masks, + new_det_thresh, + iou_threshold_trk, + iou_threshold, + HIGH_CONF_THRESH, + use_iom_recondition, + o2o_matching_masklets_enable, + iom_thresh_recondition, + iou_thresh_recondition, +): + det_masks_binary = det_masks > 0 + det_masks_binary[~det_keep] = 0 + trk_masks_binary = trk_masks > 0 + intersection_metric = None + if use_iom_recondition: + intersection_metric = mask_iom(det_masks_binary, trk_masks_binary) # (N, M) + else: + intersection_metric = mask_iou(det_masks_binary, trk_masks_binary) # (N, M) + + assert not o2o_matching_masklets_enable, "Temporarily disabled support for o2o_matching_masklets_enable, due to optimizations." + + if o2o_matching_masklets_enable: + intersection_metric_np = intersection_metric.cpu().numpy() + from scipy.optimize import linear_sum_assignment + + cost_matrix = 1 - intersection_metric_np + row_ind, col_ind = linear_sum_assignment(cost_matrix) + trk_is_matched = np.zeros(trk_masks.size(0), dtype=bool) + for d, t in zip(row_ind, col_ind): + if intersection_metric_np[d, t] >= iou_threshold_trk: + trk_is_matched[t] = True + trk_is_matched = torch.from_numpy(trk_is_matched) + trk_is_matched = trk_is_matched.to(device=intersection_metric.device) + else: + trk_is_matched = (intersection_metric >= iou_threshold_trk).any(dim=0) + # Non-empty tracks not matched by Hungarian assignment above threshold are unmatched + trk_is_nonempty = trk_masks_binary.any(dim=(1, 2)) + trk_is_unmatched = torch.logical_and(trk_is_nonempty, ~trk_is_matched) + + # For detections: allow many tracks to match to the same detection (many-to-one) + # So, a detection is 'new' if it does not match any track above threshold + is_new_det = torch.logical_and( + torch.logical_and((det_scores >= new_det_thresh), (det_keep)), + torch.logical_not(torch.any(intersection_metric >= iou_threshold, dim=1)), + ) + + intersection_thresh_recond = ( + iom_thresh_recondition if use_iom_recondition else iou_thresh_recondition + ) + # if a detection matches to many tracks with high IoU or vice versa, we do not consider it for reconditioning as it might be ambiguous + det_match_to_many_trk = (intersection_metric >= intersection_thresh_recond).sum( + dim=1 + ) > 1 + trk_match_to_many_det = (intersection_metric >= intersection_thresh_recond).sum( + dim=0 + ) > 1 + # # zero out these ambiguous matches + # intersection_metric[:, trk_match_to_many_det] = ( + # 0.0 # only consider unique matches + # ) + + # intersection_metric[det_match_to_many_trk, :] = ( + # 0.0 # only consider unique matches + # ) + + intersection_metric = torch.where( + trk_match_to_many_det.unsqueeze(0), + torch.zeros_like(intersection_metric), + intersection_metric, + ) + + intersection_metric = torch.where( + det_match_to_many_trk.unsqueeze(1), + torch.zeros_like(intersection_metric), + intersection_metric, + ) + + det_to_max_iou_trk_idx = torch.argmax(intersection_metric, dim=1) + det_is_high_conf = ((det_scores >= HIGH_CONF_THRESH) & det_keep) & ~is_new_det + det_is_high_iou = ( + torch.amax(intersection_metric, dim=1) >= intersection_thresh_recond + ) + im_mask = intersection_metric >= iou_threshold + + return ( + trk_is_unmatched, + trk_is_nonempty, + is_new_det, + det_to_max_iou_trk_idx, + det_is_high_conf, + det_is_high_iou, + det_keep, + im_mask, + ) + + class Sam3VideoBase(nn.Module): def __init__( self, @@ -516,17 +737,7 @@ def run_tracker_update_planning_phase( is_image_only: bool = False, ): # initialize new metadata from previous metadata (its values will be updated later) - tracker_metadata_new = { - "obj_ids_per_gpu": deepcopy(tracker_metadata_prev["obj_ids_per_gpu"]), - "obj_ids_all_gpu": None, # will be filled later - "num_obj_per_gpu": deepcopy(tracker_metadata_prev["num_obj_per_gpu"]), - "obj_id_to_score": deepcopy(tracker_metadata_prev["obj_id_to_score"]), - "obj_id_to_tracker_score_frame_wise": deepcopy( - tracker_metadata_prev["obj_id_to_tracker_score_frame_wise"] - ), - "obj_id_to_last_occluded": {}, # will be filled later - "max_obj_id": deepcopy(tracker_metadata_prev["max_obj_id"]), - } + tracker_metadata_new = self._create_planning_metadata(tracker_metadata_prev) # Initialize reconditioned_obj_ids early to avoid UnboundLocalError reconditioned_obj_ids = set() @@ -619,9 +830,9 @@ def run_tracker_update_planning_phase( num_obj_dropped_due_to_limit, trk_id_to_max_iou_high_conf_det, ] - assert len(update_plan) == NUM_BROADCAST_ITEMS, ( - f"Manually update NUM_BROADCAST_ITEMS to be: {len(update_plan)}" - ) + assert ( + len(update_plan) == NUM_BROADCAST_ITEMS + ), f"Manually update NUM_BROADCAST_ITEMS to be: {len(update_plan)}" self.broadcast_python_obj_cpu(update_plan, src=0) elif self.rank > 0 and self.world_size > 1: update_plan = [ @@ -841,9 +1052,9 @@ def _suppress_overlapping_based_on_recent_occlusion( binary_tracker_low_res_masks_global = tracker_low_res_masks_global > 0 batch_size = tracker_low_res_masks_global.size(0) if batch_size > 0: - assert len(obj_ids_global) == batch_size, ( - f"Mismatch in number of objects: {len(obj_ids_global)} vs {batch_size}" - ) + assert ( + len(obj_ids_global) == batch_size + ), f"Mismatch in number of objects: {len(obj_ids_global)} vs {batch_size}" NEVER_OCCLUDED = -1 ALWAYS_OCCLUDED = 100000 # This value should be larger than any possible frame index, indicates that the object was removed by hotstart logic last_occluded_prev = torch.cat( @@ -901,6 +1112,7 @@ def run_tracker_update_execution_phase( orig_vid_height: int, orig_vid_width: int, feature_cache: Dict, + tracker_metadata_new=None, ): # initialize tracking scores with detection scores new_det_fa_inds: npt.NDArray = tracker_update_plan["new_det_fa_inds"] @@ -931,8 +1143,31 @@ def run_tracker_update_execution_phase( if len(obj_ids_newly_removed) > 0: self._tracker_remove_objects(tracker_states_local, obj_ids_newly_removed) + self._post_execution_phase_hook(tracker_states_local, tracker_metadata_new) return tracker_states_local + def _create_planning_metadata(self, tracker_metadata_prev): + """Create the metadata dict for the planning phase from previous metadata.""" + from copy import deepcopy + + score_key = "obj_id_to_tracker_score_frame_wise" + if score_key not in tracker_metadata_prev: + score_key = "obj_id_to_sam2_score_frame_wise" + metadata = { + "obj_ids_per_gpu": deepcopy(tracker_metadata_prev["obj_ids_per_gpu"]), + "obj_ids_all_gpu": None, + "num_obj_per_gpu": deepcopy(tracker_metadata_prev["num_obj_per_gpu"]), + "obj_id_to_score": deepcopy(tracker_metadata_prev["obj_id_to_score"]), + score_key: deepcopy(tracker_metadata_prev[score_key]), + "obj_id_to_last_occluded": {}, + "max_obj_id": deepcopy(tracker_metadata_prev["max_obj_id"]), + } + return metadata + + def _post_execution_phase_hook(self, tracker_states_local, tracker_metadata_new): + """Hook for subclasses to add post-execution logic. Default: no-op.""" + pass + def build_outputs( self, frame_idx: int, @@ -1022,9 +1257,9 @@ def _get_objects_to_suppress_based_on_most_recently_occluded( reverse: bool = False, ): # Suppress overlapping masks for objects that were most recently occluded - assert binary_low_res_masks.dtype == torch.bool, ( - f"Expected boolean tensor, got {binary_low_res_masks.dtype}" - ) + assert ( + binary_low_res_masks.dtype == torch.bool + ), f"Expected boolean tensor, got {binary_low_res_masks.dtype}" to_suppress = torch.zeros( binary_low_res_masks.size(0), device=binary_low_res_masks.device, @@ -1129,9 +1364,9 @@ def _propogate_tracker_one_frame_local_gpu( num_frames_propagated += 1 # only 1 frames should be propagated - assert num_frames_propagated == 1 and out_frame_idx == frame_idx, ( - f"num_frames_propagated: {num_frames_propagated}, out_frame_idx: {out_frame_idx}, frame_idx: {frame_idx}" - ) + assert ( + num_frames_propagated == 1 and out_frame_idx == frame_idx + ), f"num_frames_propagated: {num_frames_propagated}, out_frame_idx: {out_frame_idx}, frame_idx: {frame_idx}" assert isinstance(out_obj_ids, list) obj_ids_local.extend(out_obj_ids) low_res_masks_list.append(out_low_res_masks.squeeze(1)) @@ -1180,6 +1415,7 @@ def _associate_det_trk( to any detections on this frame (for unmatched, we only count masklets with >0 area) - det_to_matched_trk_obj_ids: dict[int, npt.NDArray]: mapping from detector's detection indices to the list of matched tracklet object IDs + - trk_id_to_max_iou_high_conf_det: dict mapping track obj_id to the highest-IoU high-conf detection idx - empty_trk_obj_ids: array of existing masklet object IDs with zero area in SAM2 prediction """ iou_threshold = self.assoc_iou_thresh @@ -1188,9 +1424,9 @@ def _associate_det_trk( assert det_masks.is_floating_point(), "float tensor expected (do not binarize)" assert trk_masks.is_floating_point(), "float tensor expected (do not binarize)" - assert trk_masks.size(0) == len(trk_obj_ids), ( - f"trk_masks and trk_obj_ids should have the same length, {trk_masks.size(0)} vs {len(trk_obj_ids)}" - ) + assert ( + trk_masks.size(0) == len(trk_obj_ids) + ), f"trk_masks and trk_obj_ids should have the same length, {trk_masks.size(0)} vs {len(trk_obj_ids)}" if trk_masks.size(0) == 0: # all detections are new new_det_fa_inds = np.arange(det_masks.size(0)) @@ -1239,61 +1475,40 @@ def _associate_det_trk( align_corners=False, ).squeeze(1) - det_masks_binary = det_masks > 0 - trk_masks_binary = trk_masks > 0 - ious = mask_iou(det_masks_binary, trk_masks_binary) # (N, M) - - ious_np = ious.cpu().numpy() - if self.o2o_matching_masklets_enable: - from scipy.optimize import linear_sum_assignment - - # Hungarian matching for tracks (one-to-one: each track matches at most one detection) - cost_matrix = 1 - ious_np # Hungarian solves for minimum cost - row_ind, col_ind = linear_sum_assignment(cost_matrix) - trk_is_matched = np.zeros(trk_masks.size(0), dtype=bool) - for d, t in zip(row_ind, col_ind): - if ious_np[d, t] >= iou_threshold_trk: - trk_is_matched[t] = True - else: - trk_is_matched = (ious_np >= iou_threshold_trk).any(axis=0) - # Non-empty tracks not matched by Hungarian assignment above threshold are unmatched - trk_is_nonempty = trk_masks_binary.any(dim=(1, 2)).cpu().numpy() - trk_is_unmatched = np.logical_and(trk_is_nonempty, ~trk_is_matched) - unmatched_trk_obj_ids = trk_obj_ids[trk_is_unmatched] - # also record masklets that have zero area in SAM 2 prediction - empty_trk_obj_ids = trk_obj_ids[~trk_is_nonempty] - - # For detections: allow many tracks to match to the same detection (many-to-one) - # So, a detection is 'new' if it does not match any track above threshold - is_new_det = np.logical_and( - det_scores_np >= new_det_thresh, - np.logical_not(np.any(ious_np >= iou_threshold, axis=1)), + # Convert numpy scores to tensor for the compilable function + det_scores = torch.from_numpy(det_scores_np).to(det_masks.device) + det_keep = torch.ones( + det_masks.size(0), dtype=torch.bool, device=det_masks.device ) - new_det_fa_inds = np.nonzero(is_new_det)[0] - - # for each detection, which tracks it matched to (above threshold) - det_to_matched_trk_obj_ids = {} - trk_id_to_max_iou_high_conf_det = {} # trk id --> exactly one detection idx - HIGH_CONF_THRESH = 0.8 - HIGH_IOU_THRESH = 0.8 - det_to_max_iou_trk_idx = np.argmax(ious_np, axis=1) - det_is_high_conf = (det_scores_np >= HIGH_CONF_THRESH) & ~is_new_det - det_is_high_iou = np.max(ious_np, axis=1) >= HIGH_IOU_THRESH - det_is_high_conf_and_iou = set( - np.nonzero(det_is_high_conf & det_is_high_iou)[0] + + # Call the GPU-native compilable function + adt_result_tensors = _associate_det_trk_compilable( + det_masks=det_masks, + det_scores=det_scores, + det_keep=det_keep, + trk_masks=trk_masks, + new_det_thresh=new_det_thresh, + iou_threshold_trk=iou_threshold_trk, + iou_threshold=iou_threshold, + HIGH_CONF_THRESH=0.8, + use_iom_recondition=getattr(self, "use_iom_recondition", False), + o2o_matching_masklets_enable=self.o2o_matching_masklets_enable, + iom_thresh_recondition=getattr(self, "iom_thresh_recondition", 0.8), + iou_thresh_recondition=getattr(self, "iou_thresh_recondition", 0.8), ) - for d in range(det_masks.size(0)): - det_to_matched_trk_obj_ids[d] = trk_obj_ids[ious_np[d, :] >= iou_threshold] - if d in det_is_high_conf_and_iou: - trk_obj_id = trk_obj_ids[det_to_max_iou_trk_idx[d]].item() - trk_id_to_max_iou_high_conf_det[trk_obj_id] = d + + # Wrap in LazyAssociateDetTrkResult and immediately realize to numpy + # for backward compatibility with existing callers + lazy_result = LazyAssociateDetTrkResult(*adt_result_tensors) + lazy_result._convert_to_numpy() + realized = lazy_result._create_cpu_metadata(trk_obj_ids, det_masks) return ( - new_det_fa_inds, - unmatched_trk_obj_ids, - det_to_matched_trk_obj_ids, - trk_id_to_max_iou_high_conf_det, - empty_trk_obj_ids, + realized.new_det_fa_inds, + realized.unmatched_trk_obj_ids, + realized.det_to_matched_trk_obj_ids, + realized.trk_id_to_max_iou_high_conf_det, + realized.empty_trk_obj_ids, ) def _assign_new_det_to_gpus(self, new_det_num, prev_workload_per_gpu): @@ -1601,21 +1816,35 @@ def _tracker_remove_objects( def _initialize_metadata(self): """Initialize metadata for the masklets.""" + is_multiplex = getattr(self, "is_multiplex", False) + score_key = ( + "obj_id_to_sam2_score_frame_wise" + if is_multiplex + else "obj_id_to_tracker_score_frame_wise" + ) tracker_metadata = { "obj_ids_per_gpu": [np.array([], np.int64) for _ in range(self.world_size)], "obj_ids_all_gpu": np.array([], np.int64), "num_obj_per_gpu": np.zeros(self.world_size, np.int64), "max_obj_id": -1, "obj_id_to_score": {}, - "obj_id_to_tracker_score_frame_wise": defaultdict(dict), + score_key: defaultdict(dict), "obj_id_to_last_occluded": {}, } - if self.rank == 0: - # "rank0_metadata" contains metadata that is only stored on (and accessible to) GPU 0 - # - obj_first_frame_idx: obj_id --> first frame index where the object was detected - # - unmatched_frame_inds: obj_id --> [mismatched frame indices] - # - overlap_pair_to_frame_inds: (first_appear_obj_id, obj_id) --> [overlap frame indices] - # - removed_obj_ids: object IDs that are suppressed via hot-start + if is_multiplex: + tracker_metadata["gpu_metadata"] = { + "N_obj": 0 + } # GPU-side metadata for sync-free hotstart + tracker_metadata["num_buc_per_gpu"] = np.zeros(self.world_size, np.int64) + + # "rank0_metadata" contains metadata that is only stored on (and accessible to) GPU 0 + # - obj_first_frame_idx: obj_id --> first frame index where the object was detected + # - unmatched_frame_inds: obj_id --> [mismatched frame indices] + # - overlap_pair_to_frame_inds: (first_appear_obj_id, obj_id) --> [overlap frame indices] + # - removed_obj_ids: object IDs that are suppressed via hot-start + # In multiplex mode, rank0_metadata is always included (all GPUs need it). + # In non-multiplex mode, only rank 0 stores it. + if is_multiplex or self.rank == 0: rank0_metadata = { "obj_first_frame_idx": {}, "unmatched_frame_inds": defaultdict(list), @@ -1654,9 +1883,9 @@ def update_masklet_confirmation_status( # a) first, expand "confirmation_data" to include new masklets added in this frame status_prev = confirmation_data["status"] consecutive_det_num_prev = confirmation_data["consecutive_det_num"] - assert status_prev.shape == obj_ids_all_gpu_prev.shape, ( - f"Got {status_prev.shape} vs {obj_ids_all_gpu_prev.shape}" - ) + assert ( + status_prev.shape == obj_ids_all_gpu_prev.shape + ), f"Got {status_prev.shape} vs {obj_ids_all_gpu_prev.shape}" obj_id_to_updated_idx = { obj_id: idx for idx, obj_id in enumerate(obj_ids_all_gpu_updated) diff --git a/sam3/model/sam3_video_inference.py b/sam3/model/sam3_video_inference.py index 18cd387..f707505 100644 --- a/sam3/model/sam3_video_inference.py +++ b/sam3/model/sam3_video_inference.py @@ -554,9 +554,7 @@ def _build_tracker_output( assert ( "cached_frame_outputs" in inference_state and frame_idx in inference_state["cached_frame_outputs"] - ), ( - "No cached outputs found. Ensure normal propagation has run first to populate the cache." - ) + ), "No cached outputs found. Ensure normal propagation has run first to populate the cache." cached_outputs = inference_state["cached_frame_outputs"][frame_idx] obj_id_to_mask = cached_outputs.copy() @@ -564,9 +562,9 @@ def _build_tracker_output( # Update with refined masks if provided if refined_obj_id_to_mask is not None: for obj_id, refined_mask in refined_obj_id_to_mask.items(): - assert refined_mask is not None, ( - f"Refined mask data must be provided for obj_id {obj_id}" - ) + assert ( + refined_mask is not None + ), f"Refined mask data must be provided for obj_id {obj_id}" obj_id_to_mask[obj_id] = refined_mask return obj_id_to_mask @@ -855,12 +853,12 @@ def add_prompt( logger.debug("Running add_prompt on frame %d", frame_idx) num_frames = inference_state["num_frames"] - assert text_str is not None or boxes_xywh is not None, ( - "at least one type of prompt (text, boxes) must be provided" - ) - assert 0 <= frame_idx < num_frames, ( - f"{frame_idx=} is out of range for a total of {num_frames} frames" - ) + assert ( + text_str is not None or boxes_xywh is not None + ), "at least one type of prompt (text, boxes) must be provided" + assert ( + 0 <= frame_idx < num_frames + ), f"{frame_idx=} is out of range for a total of {num_frames} frames" # since it's a semantic prompt, we start over self.reset_state(inference_state) @@ -1201,9 +1199,9 @@ def add_action_history( "propagation_partial", "propagation_fetch", ] - assert action_type in instance_actions + propagation_actions, ( - f"Invalid action type: {action_type}, must be one of {instance_actions + propagation_actions}" - ) + assert ( + action_type in instance_actions + propagation_actions + ), f"Invalid action type: {action_type}, must be one of {instance_actions + propagation_actions}" action = { "type": action_type, "frame_idx": frame_idx, @@ -1371,12 +1369,12 @@ def add_prompt( ): if points is not None: # Tracker instance prompts - assert text_str is None and boxes_xywh is None, ( - "When points are provided, text_str and boxes_xywh must be None." - ) - assert obj_id is not None, ( - "When points are provided, obj_id must be provided." - ) + assert ( + text_str is None and boxes_xywh is None + ), "When points are provided, text_str and boxes_xywh must be None." + assert ( + obj_id is not None + ), "When points are provided, obj_id must be provided." return self.add_tracker_new_points( inference_state, frame_idx, @@ -1492,9 +1490,9 @@ def add_tracker_new_points( tracker_states = self._get_tracker_inference_states_by_obj_ids( inference_state, [obj_id] ) - assert len(tracker_states) == 1, ( - f"[rank={self.rank}] Multiple Tracker inference states found for the same object id." - ) + assert ( + len(tracker_states) == 1 + ), f"[rank={self.rank}] Multiple Tracker inference states found for the same object id." tracker_state = tracker_states[0] # log diff --git a/sam3/model/sam3_video_predictor.py b/sam3/model/sam3_video_predictor.py index ce3b801..a7660af 100644 --- a/sam3/model/sam3_video_predictor.py +++ b/sam3/model/sam3_video_predictor.py @@ -17,14 +17,12 @@ import psutil import torch from sam3.logger import get_logger +from sam3.model.sam3_base_predictor import Sam3BasePredictor logger = get_logger(__name__) -class Sam3VideoPredictor: - # a global dictionary that holds all inference states for this model (key is session_id) - _ALL_INFERENCE_STATES = {} - +class Sam3VideoPredictor(Sam3BasePredictor): def __init__( self, checkpoint_path=None, @@ -37,6 +35,7 @@ def __init__( apply_temporal_disambiguation: bool = True, compile: bool = False, ): + super().__init__() self.async_loading_frames = async_loading_frames self.video_loader_type = video_loader_type from sam3.model_builder import build_sam3_video_model @@ -55,124 +54,14 @@ def __init__( .eval() ) - @torch.inference_mode() - def handle_request(self, request): - """Dispatch a request based on its type.""" - request_type = request["type"] - if request_type == "start_session": - return self.start_session( - resource_path=request["resource_path"], - session_id=request.get("session_id", None), - ) - elif request_type == "add_prompt": - return self.add_prompt( - session_id=request["session_id"], - frame_idx=request["frame_index"], - text=request.get("text", None), - points=request.get("points", None), - point_labels=request.get("point_labels", None), - bounding_boxes=request.get("bounding_boxes", None), - bounding_box_labels=request.get("bounding_box_labels", None), - obj_id=request.get("obj_id", None), - ) - elif request_type == "remove_object": - return self.remove_object( - session_id=request["session_id"], - obj_id=request["obj_id"], - is_user_action=request.get("is_user_action", True), - ) - elif request_type == "reset_session": - return self.reset_session(session_id=request["session_id"]) - elif request_type == "close_session": - return self.close_session(session_id=request["session_id"]) - else: - raise RuntimeError(f"invalid request type: {request_type}") - - @torch.inference_mode() - def handle_stream_request(self, request): - """Dispatch a stream request based on its type.""" - request_type = request["type"] - if request_type == "propagate_in_video": - yield from self.propagate_in_video( - session_id=request["session_id"], - propagation_direction=request.get("propagation_direction", "both"), - start_frame_idx=request.get("start_frame_index", None), - max_frame_num_to_track=request.get("max_frame_num_to_track", None), - ) - else: - raise RuntimeError(f"invalid request type: {request_type}") - - def start_session(self, resource_path, session_id=None): - """ - Start a new inference session on an image or a video. Here `resource_path` - can be either a path to an image file (for image inference) or an MP4 file - or directory with JPEG video frames (for video inference). - - If `session_id` is defined, it will be used as identifier for the - session. If it is not defined, the start_session function will create - a session id and return it. - """ - # get an initial inference_state from the model - inference_state = self.model.init_state( - resource_path=resource_path, - async_loading_frames=self.async_loading_frames, - video_loader_type=self.video_loader_type, - ) - if not session_id: - session_id = str(uuid.uuid4()) - self._ALL_INFERENCE_STATES[session_id] = { - "state": inference_state, - "session_id": session_id, - "start_time": time.time(), - } - logger.debug( - f"started new session {session_id}; {self._get_session_stats()}; " - f"{self._get_torch_and_gpu_properties()}" - ) - return {"session_id": session_id} - - def add_prompt( - self, - session_id: str, - frame_idx: int, - text: Optional[str] = None, - points: Optional[List[List[float]]] = None, - point_labels: Optional[List[int]] = None, - bounding_boxes: Optional[List[List[float]]] = None, - bounding_box_labels: Optional[List[int]] = None, - obj_id: Optional[int] = None, - ): - """Add text, box and/or point prompt on a specific video frame.""" - logger.debug( - f"add prompt on frame {frame_idx} in session {session_id}: " - f"{text=}, {points=}, {point_labels=}, " - f"{bounding_boxes=}, {bounding_box_labels=}" - ) - session = self._get_session(session_id) - inference_state = session["state"] - - frame_idx, outputs = self.model.add_prompt( - inference_state=inference_state, - frame_idx=frame_idx, - text_str=text, - points=points, - point_labels=point_labels, - boxes_xywh=bounding_boxes, - box_labels=bounding_box_labels, - obj_id=obj_id, - ) - return {"frame_index": frame_idx, "outputs": outputs} - def remove_object( self, session_id: str, - obj_id: int, + frame_idx: int = 0, + obj_id: int = 0, is_user_action: bool = True, ): - """Remove an object from tracking.""" - logger.debug( - f"remove object {obj_id} in session {session_id}: {is_user_action=}" - ) + """Remove an object from tracking (SAM3 uses a simpler remove_object API).""" session = self._get_session(session_id) inference_state = session["state"] @@ -183,111 +72,29 @@ def remove_object( ) return {"is_success": True} - def propagate_in_video( - self, - session_id, - propagation_direction, - start_frame_idx, - max_frame_num_to_track, - ): - """Propagate the added prompts to get grounding results on all video frames.""" - logger.debug( - f"propagate in video in session {session_id}: " - f"{propagation_direction=}, {start_frame_idx=}, {max_frame_num_to_track=}" - ) - try: - session = self._get_session(session_id) - inference_state = session["state"] - if propagation_direction not in ["both", "forward", "backward"]: - raise ValueError( - f"invalid propagation direction: {propagation_direction}" - ) - - # First doing the forward propagation - if propagation_direction in ["both", "forward"]: - for frame_idx, outputs in self.model.propagate_in_video( - inference_state=inference_state, - start_frame_idx=start_frame_idx, - max_frame_num_to_track=max_frame_num_to_track, - reverse=False, - ): - yield {"frame_index": frame_idx, "outputs": outputs} - # Then doing the backward propagation (reverse in time) - if propagation_direction in ["both", "backward"]: - for frame_idx, outputs in self.model.propagate_in_video( - inference_state=inference_state, - start_frame_idx=start_frame_idx, - max_frame_num_to_track=max_frame_num_to_track, - reverse=True, - ): - yield {"frame_index": frame_idx, "outputs": outputs} - finally: - # Log upon completion (so that e.g. we can see if two propagations happen in parallel). - # Using `finally` here to log even when the tracking is aborted with GeneratorExit. - logger.debug( - f"propagation ended in session {session_id}; {self._get_session_stats()}" - ) - - def reset_session(self, session_id): - """Reset the session to its initial state (as when it's initial opened).""" - logger.debug(f"reset session {session_id}") - session = self._get_session(session_id) - inference_state = session["state"] - self.model.reset_state(inference_state) - return {"is_success": True} - - def close_session(self, session_id): - """ - Close a session. This method is idempotent and can be called multiple - times on the same "session_id". - """ - session = self._ALL_INFERENCE_STATES.pop(session_id, None) - if session is None: - logger.warning( - f"cannot close session {session_id} as it does not exist (it might have expired); " - f"{self._get_session_stats()}" - ) - else: - del session - gc.collect() - logger.info(f"removed session {session_id}; {self._get_session_stats()}") - return {"is_success": True} - - def _get_session(self, session_id): - session = self._ALL_INFERENCE_STATES.get(session_id, None) - if session is None: - raise RuntimeError( - f"Cannot find session {session_id}; it might have expired" - ) - return session - def _get_session_stats(self): """Get a statistics string for live sessions and their GPU usage.""" - # print both the session ids and their video frame numbers - live_session_strs = [ - f"'{session_id}' ({session['state']['num_frames']} frames)" - for session_id, session in self._ALL_INFERENCE_STATES.items() - ] - session_stats_str = ( - f"live sessions: [{', '.join(live_session_strs)}], GPU memory: " - f"{torch.cuda.memory_allocated() // 1024**2} MiB used and " - f"{torch.cuda.memory_reserved() // 1024**2} MiB reserved" - f" (max over time: {torch.cuda.max_memory_allocated() // 1024**2} MiB used " - f"and {torch.cuda.max_memory_reserved() // 1024**2} MiB reserved)" + live_session_strs = [] + for sid, s in self._all_inference_states.items(): + nf = s["state"]["num_frames"] + live_session_strs.append(f"'{sid}' ({nf} frames)") + joined = ", ".join(live_session_strs) + mem_alloc = torch.cuda.memory_allocated() // 1024**2 + mem_res = torch.cuda.memory_reserved() // 1024**2 + max_alloc = torch.cuda.max_memory_allocated() // 1024**2 + max_res = torch.cuda.max_memory_reserved() // 1024**2 + return ( + f"live sessions: [{joined}], GPU memory: " + f"{mem_alloc} MiB used and {mem_res} MiB reserved" + f" (max over time: {max_alloc} MiB used and {max_res} MiB reserved)" ) - return session_stats_str def _get_torch_and_gpu_properties(self): - """Get a string for PyTorch and GPU properties (for logging and debugging).""" - torch_and_gpu_str = ( + """Get a string for PyTorch and GPU properties.""" + return ( f"torch: {torch.__version__} with CUDA arch {torch.cuda.get_arch_list()}, " f"GPU device: {torch.cuda.get_device_properties(torch.cuda.current_device())}" ) - return torch_and_gpu_str - - def shutdown(self): - """Shutdown the predictor and clear all sessions.""" - self._ALL_INFERENCE_STATES.clear() class Sam3VideoPredictorMultiGPU(Sam3VideoPredictor): diff --git a/sam3/model/text_encoder_ve.py b/sam3/model/text_encoder_ve.py index 5788358..53ddd5d 100644 --- a/sam3/model/text_encoder_ve.py +++ b/sam3/model/text_encoder_ve.py @@ -318,9 +318,9 @@ def forward( # The text is already encoded, use as is. text_attention_mask, text_memory_resized, tokenized = text inputs_embeds = tokenized["inputs_embeds"] - assert input_boxes is None or len(input_boxes) == 0, ( - "Can't replace boxes in text if it's already encoded" - ) + assert ( + input_boxes is None or len(input_boxes) == 0 + ), "Can't replace boxes in text if it's already encoded" # Note that the input_embeds are returned in pytorch's convention (sequence first) return ( diff --git a/sam3/model/video_tracking_multiplex.py b/sam3/model/video_tracking_multiplex.py new file mode 100644 index 0000000..0a4b0eb --- /dev/null +++ b/sam3/model/video_tracking_multiplex.py @@ -0,0 +1,3654 @@ +from collections import defaultdict + +""" +Video tracking model with multiplexing support. + +This file extends the base video tracking with prompt functionality to add: + - Multiplexing: Support for processing multiple objects simultaneously + - Recording image features in memory to support the decoupled transformer for memory reading +""" + +import logging +from copy import deepcopy + +try: + from typing import Iterable, Literal, NotRequired, Optional, Required, TypedDict +except ImportError: + from typing_extensions import ( + Iterable, + Literal, + NotRequired, # not available in Python 3.10 + Optional, + Required, # not available in Python 3.10 + TypedDict, + ) + +import numpy as np +import torch +import torch.distributed +import torch.nn as nn +import torch.nn.functional as F +from sam3.model.data_misc import BatchedDatapoint, NestedTensor +from sam3.model.memory import SimpleMaskEncoder +from sam3.model.multiplex_mask_decoder import MLP, MultiplexMaskDecoder +from sam3.model.multiplex_utils import MultiplexController, MultiplexState +from sam3.model.sam3_tracker_utils import ( + get_1d_sine_pe, + get_next_point, + sample_box_points, + select_closest_cond_frames, +) +from sam3.sam.mask_decoder import MaskDecoder +from sam3.sam.prompt_encoder import PositionEmbeddingRandom, PromptEncoder +from sam3.sam.transformer import TwoWayTransformer +from timm.models.layers import trunc_normal_ + + +# a large negative value as a placeholder score for missing objects +NO_OBJ_SCORE = -1024.0 + +neck_outs = ["interactive", "sam2_backbone_out"] + + +class SAMOutput(TypedDict, total=True): + # Outputs from a single SAM head forward + low_res_multimasks: torch.Tensor + high_res_multimasks: torch.Tensor + ious: torch.Tensor + low_res_masks: torch.Tensor + high_res_masks: torch.Tensor + object_score_logits: torch.Tensor + obj_ptr: NotRequired[torch.Tensor] # [num_objects, C], in data space + + +class StageOutput(TypedDict, total=False): + # metadata + conditioning_objects: Required[set[int]] + + # The outputs from a single stage; could be used as memory + pred_masks: torch.Tensor + pred_masks_high_res: torch.Tensor + point_inputs: dict[str, torch.Tensor] + mask_inputs: torch.Tensor + object_score_logits: torch.Tensor + obj_ptr: torch.Tensor # [num_buckets, multiplex_count, C], in mux space + maskmem_features: torch.Tensor + maskmem_pos_enc: list[torch.Tensor] + image_features: torch.Tensor + image_pos_enc: torch.Tensor + + # for memory filtering + iou_score: torch.Tensor + eff_iou_score: torch.Tensor + + # Multi-step prediction fields for state tracking or training + multistep_pred_masks: torch.Tensor + multistep_pred_masks_high_res: torch.Tensor + multistep_pred_multimasks: list[torch.Tensor] + multistep_pred_multimasks_high_res: list[torch.Tensor] + multistep_pred_ious: list[torch.Tensor] + multistep_point_inputs: list[dict] + multistep_object_score_logits: list[torch.Tensor] + + +class VideoTrackingMultiplex(nn.Module): + def __init__( + self, + backbone: nn.Module, + transformer: nn.Module, + maskmem_backbone: nn.Module, + multiplex_controller: MultiplexController, + num_maskmem: int = 7, # default 1 input frame + 6 previous frames as in CAE + image_size: int = 512, + backbone_stride: int = 16, # default to 16 as in CAE (truncated Hiera backbone) + prob_to_use_pt_input_for_train: float = 0.0, + prob_to_use_pt_input_for_eval: float = 0.0, + prob_to_use_box_input_for_train: float = 0.0, + prob_to_use_box_input_for_eval: float = 0.0, + # always_keep_first_frame_mem=True, # this option is removed (we've always set it to True) + apply_sigmoid_to_mask_logits_for_mem_enc: bool = False, + sigmoid_scale_for_mem_enc: float = 1.0, # scale factor for mask sigmoid prob, only effective when `apply_sigmoid_to_mask_logits_for_mem_enc` is True + sigmoid_bias_for_mem_enc: float = 0.0, # bias factor for mask sigmoid prob, only effective when `apply_sigmoid_to_mask_logits_for_mem_enc` is True + # During evaluation, whether to binarize the sigmoid mask logits on interacted frames with clicks, only effective when `apply_sigmoid_to_mask_logits_for_mem_enc` is True + binarize_mask_from_pts_for_mem_enc: bool = False, + use_mask_input_as_output_without_sam: bool = False, # on frames with mask input, whether to directly output the input mask without using a SAM prompt encoder + mask decoder + # how many frames for interactive point sampling (only effective when using point inputs per video; the first frame is always used) + # - if `num_frames_to_correct` below is True, we randomly sample 1~num_frames_to_correct frames for interactive point sampling + # - otherwise we used a fixed number of num_frames_to_correct frames for interactive point sampling + # if it is 1, we do interactive point sampling only on the 1st frame + # if it is greater than 1, we interactive point sampling in the 1st frame and other randomly selected frames + num_frames_to_correct_for_train: int = 1, # default: only iteratively sample on first frame + num_frames_to_correct_for_eval: int = 1, # default: only iteratively sample on first frame + rand_frames_to_correct_for_train: bool = False, + rand_frames_to_correct_for_eval: bool = False, + prob_correct_all_objects_for_train: float = 0.0, + ratio_of_objects_to_correct_for_train: float = 1.0, + force_correct_all_for_conditional_inputs: bool = False, + rand_objects_to_correct_for_train: bool = True, + # how many frames to use as initial conditioning frames (for both point input and mask input; the first frame is always used as an initial conditioning frame) + # - if `rand_init_cond_frames` below is True, we randomly sample 1~num_init_cond_frames initial conditioning frames + # - otherwise we sample a fixed number of num_init_cond_frames initial conditioning frames + # note: for point input, we sample correction points on all such initial conditioning frames, and we require that `num_frames_to_correct` >= `num_init_cond_frames`; + # these are initial conditioning frames because as we track the video, more conditioning frames might be added + # when a frame receives correction clicks under point input if `add_all_frames_to_correct_as_cond=True` + num_init_cond_frames_for_train: int = 1, # default: only use the first frame as initial conditioning frame + num_init_cond_frames_for_eval: int = 1, # default: only use the first frame as initial conditioning frame + rand_init_cond_frames_for_train: bool = True, # default: random 1~num_init_cond_frames_for_train cond frames (to be constent w/ previous TA data loader) + rand_init_cond_frames_for_eval: bool = False, + # The maximum number of conditioning frames to participate in the memory attention (-1 means no limit; if there are more conditioning frames than this limit, + # we only cross-attend to the temporally closest `max_cond_frames_in_attn` conditioning frames in the encoder when tracking each frame). This gives the model + # a temporal locality when handling a large number of annotated frames (since closer frames should be more important) and also avoids GPU OOM. + max_cond_frames_in_attn: int = -1, + # Whether to always keep the first conditioning frame in case we exceed the maximum number of conditioning frames allowed + keep_first_cond_frame=False, + # if `add_all_frames_to_correct_as_cond` is True, we also append to the conditioning frame list any frame that receives a later correction click + # if `add_all_frames_to_correct_as_cond` is False, we conditioning frame list to only use those initial conditioning frames + add_all_frames_to_correct_as_cond: bool = False, + # how many additional correction points to sample (on each frame selected to be corrected) + # note that the first frame receives an initial input click (in addition to any correction clicks) + num_correction_pt_per_frame: int = 7, + # method for point sampling during evaluation + # "uniform" (sample uniformly from error region) or "center" (use the point with the largest distance to error region boundary) + # default to "center" to be consistent with evaluation in the SAM paper + pt_sampling_for_eval: Literal["uniform", "center"] = "center", + # During training, we optionally allow sampling the correction points from GT regions + # instead of the prediction error regions with a small probability. This might allow the + # model to overfit less to the error regions in training datasets + prob_to_sample_from_gt_for_train: float = 0.0, + # on the first frame, whether to directly add the no-memory embedding to the image feature + # (instead of using the transformer encoder) + directly_add_no_mem_embed: bool = False, + # whether to use high-resolution feature maps in the SAM mask decoder + use_high_res_features_in_sam: bool = False, + # whether to output multiple (3) masks for the first click on initial conditioning frames + multimask_output_in_sam: bool = False, + # the minimum and maximum number of clicks to use multimask_output_in_sam (only relevant when `multimask_output_in_sam=True`; + # default is 1 for both, meaning that only the first click gives multimask output; also note that a box counts as two points) + multimask_min_pt_num: int = 1, + multimask_max_pt_num: int = 1, + # whether to also use multimask output for tracking (not just for the first click on initial conditioning frames; only relevant when `multimask_output_in_sam=True`) + multimask_output_for_tracking: bool = False, + # Whether to use multimask tokens for obj ptr; Only relevant when both + # use_obj_ptrs_in_encoder=True and multimask_output_for_tracking=True + use_multimask_token_for_obj_ptr: bool = False, + # if the last output is multimask during training, whether to select the mask w/ highest IoU to the ground-truth for memory encoder + # (instead of the mask with the highest prediction score; this resembles teacher-forcing for multi-mask prediction in tracking) + use_best_iou_mask_for_mem_enc: bool = False, + # whether to use sigmoid to restrict ious prediction to [0-1] + iou_prediction_use_sigmoid: bool = False, + # whether to feed the previously predicted low-res mask logits as a mask prompt into the SAM mask decoder during iterative point sampling + iter_use_prev_mask_pred: bool = False, + # whether to forward image features per frame (as it's being tracked) during evaluation, instead of forwarding image features + # of all frames at once. This avoids backbone OOM errors on very long videos in evaluation, but could be slightly slower. + forward_backbone_per_frame_for_eval: bool = False, + # The memory bank's temporal stride during evaluation (i.e. the `r` parameter in XMem and Cutie; XMem and Cutie use r=5). + # For r>1, the (self.num_maskmem - 1) non-conditioning memory frames consist of + # (self.num_maskmem - 2) nearest frames from every r-th frames, plus the last frame. + memory_temporal_stride_for_eval: int = 1, + # whether to offload outputs to CPU memory during evaluation, to avoid GPU OOM on very long videos or very large resolutions or too many objects + # (it's recommended to use `forward_backbone_per_frame_for_eval=True` first before setting this option to True) + offload_output_to_cpu_for_eval: bool = False, + # whether to trim the output of past non-conditioning frames (num_maskmem frames before the current frame) during evaluation + # (this helps save GPU or CPU memory on very long videos for semi-supervised VOS eval, where only the first frame receives prompts) + trim_past_non_cond_mem_for_eval: bool = False, + # whether to apply non-overlapping constraints on the object masks in the memory encoder during evaluation (to avoid/alleviate superposing masks) + non_overlap_masks_for_mem_enc: bool = False, + # whether to cross-attend to object pointers from other frames (based on SAM output tokens) in the encoder + use_obj_ptrs_in_encoder: bool = False, + # the maximum number of object pointers from other frames in encoder cross attention (only relevant when `use_obj_ptrs_in_encoder=True`) + max_obj_ptrs_in_encoder: int = 16, + # whether to add temporal positional encoding to the object pointers in the encoder (only relevant when `use_obj_ptrs_in_encoder=True`) + add_tpos_enc_to_obj_ptrs: bool = True, + # whether to add an extra linear projection layer for the temporal positional encoding in the object pointers to avoid potential interference + # with spatial positional encoding (only relevant when both `use_obj_ptrs_in_encoder=True` and `add_tpos_enc_to_obj_ptrs=True`) + proj_tpos_enc_in_obj_ptrs: bool = False, + # whether to use signed distance (instead of unsigned absolute distance) in the temporal positional encoding in the object pointers + # (only relevant when both `use_obj_ptrs_in_encoder=True` and `add_tpos_enc_to_obj_ptrs=True`) + use_signed_tpos_enc_to_obj_ptrs: bool = False, + # whether to only attend to object pointers in the past (before the current frame) in the encoder during evaluation + # (only relevant when `use_obj_ptrs_in_encoder=True`; this might avoid pointer information too far in the future to distract the initial tracking) + only_obj_ptrs_in_the_past_for_eval: bool = False, + # Whether to predict if there is an object in the frame + pred_obj_scores: bool = False, + # Whether to use an MLP to predict object scores + pred_obj_scores_mlp: bool = False, + # Only relevant if pred_obj_scores=True and use_obj_ptrs_in_encoder=True; + # Whether to have a fixed no obj pointer when there is no object present + # or to use it as an additive embedding with obj_ptr produced by decoder + fixed_no_obj_ptr: bool = False, + use_no_obj_ptr: bool = True, + use_mlp_for_obj_ptr_proj: bool = False, + # replace per-slot static no-obj embeddings with linear projections of object embeddings + use_linear_no_obj_ptr: bool = False, + # add no obj embedding to spatial frames + no_obj_embed_spatial: bool = False, + # does not apply to spatial memories (only to obj ptrs), unless unified_tpos_enc=True + sincos_tpos_enc: bool = True, + # extra arguments used to construct the SAM mask decoder; if not None, it should be a dict of kwargs to be passed into `MaskDecoder` class. + sam_mask_decoder_extra_args: Optional[dict] = None, + # whether to compile all the model compoents + compile_all_components: bool = False, + # save and use image features in the memory + save_image_features: bool = False, + # number of multimask outputs in the SAM mask decoder + num_multimask_outputs: int = 3, + # use a single mask token to predict all masks + decode_mask_with_shared_tokens: bool = False, + # use the mask token for predicting ious and object scores + decode_mask_attribute_with_shared_tokens: bool = False, + share_necks: bool = False, # share the interactive and sam2_backbone necks + # if enabled, use a different rng generator for operations that differ between GPUs, + # such that the base rng that controls flow does not go out-of-sync among GPUs + # There will be a slight performance penalty when turned off due to uneven workload but it's minor + randomness_fix: bool = False, + # add a learnable embeddings to the object queries that corresponding to paddings/removed objects + add_output_suppression_embeddings: bool = False, + # add a per-object embedding to the spatial memory features if that object is a conditioning input + add_object_conditional_embeddings: bool = False, + # if None, follow add_object_conditional_embeddings + add_object_unconditional_embeddings: Optional[bool] = None, + # for each object, add an additional channel in the mask encoder to indicate conditional/unconditional objects + condition_as_mask_input: bool = False, + condition_as_mask_input_fg: float = 1.0, + condition_as_mask_input_bg: float = 0.0, + # use v2 memory positional encodings + # in v2, the last slot in the positional encoding no longer refers to the conditional frame + # it now refers to "out-of-bound" frames. + # The motivation is to shift all encodings of "conditioning" to the object_conditional embeddings + use_maskmem_tpos_v2: bool = False, + # select the frame with object existence + use_memory_selection: bool = False, + # when using memory selection, the threshold to determine if the frame is good + mf_threshold: float = 0.01, + # this is a flag for demo purposes; it does not need to be explicitly set + is_dynamic_model: bool = False, + object_score_logit_threshold: float = 0.0, + stability_score_attentuation: bool = False, # select from multimask based on iou*stability_score + ): + super().__init__() + + # the interactive sam mask deocder can use dynamic_multimask_via_stability + interactive_sam_mask_decoder_extra_args = deepcopy(sam_mask_decoder_extra_args) + if sam_mask_decoder_extra_args is not None: + dynamic_multimask_via_stability = sam_mask_decoder_extra_args.get( + "dynamic_multimask_via_stability", False + ) + if dynamic_multimask_via_stability: + sam_mask_decoder_extra_args["dynamic_multimask_via_stability"] = False + print( + "dynamic_multimask_via_stability is reset to False in the multiplex model" + ) + + # Part 1: the image backbone + self.backbone = backbone + # Use level 0, 1, 2 for high-res setting, or just level 2 for the default setting + self.use_high_res_features_in_sam = use_high_res_features_in_sam + self.num_feature_levels = 3 if use_high_res_features_in_sam else 1 + self.use_obj_ptrs_in_encoder = use_obj_ptrs_in_encoder + self.max_obj_ptrs_in_encoder = max_obj_ptrs_in_encoder + if use_obj_ptrs_in_encoder: + # A conv layer to downsample the GT mask prompt to stride 4 (the same stride as + # low-res SAM mask logits) and to change its scales from 0~1 to SAM logit scale, + # so that it can be fed into the SAM mask decoder to generate a pointer. + self.interactive_mask_downsample = torch.nn.Conv2d( + 1, 1, kernel_size=4, stride=4 + ) + + self.add_tpos_enc_to_obj_ptrs = add_tpos_enc_to_obj_ptrs + if proj_tpos_enc_in_obj_ptrs: + assert add_tpos_enc_to_obj_ptrs # these options need to be used together + self.proj_tpos_enc_in_obj_ptrs = proj_tpos_enc_in_obj_ptrs + self.use_signed_tpos_enc_to_obj_ptrs = use_signed_tpos_enc_to_obj_ptrs + self.only_obj_ptrs_in_the_past_for_eval = only_obj_ptrs_in_the_past_for_eval + self.multiplex_controller = multiplex_controller + self.save_image_features = save_image_features + self.multiplex_count = self.multiplex_controller.multiplex_count + + # Part 2: encoder-only transformer to fuse current frame's visual features + # with memories from past frames + assert transformer.decoder is None, "transformer should be encoder-only" + self.transformer = transformer + self.hidden_dim: int = transformer.d_model + + # Part 3: memory encoder for the previous frame's outputs + self.maskmem_backbone = maskmem_backbone + self.mem_dim = self.hidden_dim + if hasattr(self.maskmem_backbone, "out_proj") and hasattr( + self.maskmem_backbone.out_proj, "weight" + ): + # if there is compression of memories along channel dim + mem_dim = self.maskmem_backbone.out_proj.weight.shape[0] + assert ( + mem_dim == self.hidden_dim + ), "there should be no compression of memory embeddings" + self.num_maskmem = num_maskmem # Number of memories accessible + # Temporal encoding of the memories + self.sincos_tpos_enc = sincos_tpos_enc + self.use_maskmem_tpos_v2 = use_maskmem_tpos_v2 + # tpos specific to spatial memories only + # last token actually corresponds to conditioning + # frame embedding, indep of temporal position + self.maskmem_tpos_enc = torch.nn.Parameter( + torch.zeros(num_maskmem, 1, 1, self.mem_dim) + ) + trunc_normal_(self.maskmem_tpos_enc, std=0.02) + + # a single token to indicate no memory embedding from previous frames + self.interactivity_no_mem_embed = torch.nn.Parameter( + torch.zeros(1, 1, self.hidden_dim) + ) + trunc_normal_(self.interactivity_no_mem_embed, std=0.02) + self.directly_add_no_mem_embed = directly_add_no_mem_embed + + # Whether to apply sigmoid to the output raw mask logits (to turn them from + # range (-inf, +inf) to range (0, 1)) before feeding them into the memory encoder + self.apply_sigmoid_to_mask_logits_for_mem_enc = ( + apply_sigmoid_to_mask_logits_for_mem_enc + ) + if apply_sigmoid_to_mask_logits_for_mem_enc: + self.sigmoid_scale_for_mem_enc = sigmoid_scale_for_mem_enc + self.sigmoid_bias_for_mem_enc = sigmoid_bias_for_mem_enc + + if binarize_mask_from_pts_for_mem_enc: + logging.warning( + """ + The current model is not trained with binarize_mask_from_pts_for_mem_enc; + We force it to False here because external callers often hardcoded this + to True, ignoring the config. + Re-training should be possible. + """ + ) + binarize_mask_from_pts_for_mem_enc = False + + self.binarize_mask_from_pts_for_mem_enc = binarize_mask_from_pts_for_mem_enc + self.non_overlap_masks_for_mem_enc = non_overlap_masks_for_mem_enc + self.memory_temporal_stride_for_eval = memory_temporal_stride_for_eval + # On frames with mask input, whether to directly output the input mask without + # using a SAM prompt encoder + mask decoder + self.use_mask_input_as_output_without_sam = use_mask_input_as_output_without_sam + self.multimask_output_in_sam = multimask_output_in_sam + self.multimask_min_pt_num = multimask_min_pt_num + self.multimask_max_pt_num = multimask_max_pt_num + self.multimask_output_for_tracking = multimask_output_for_tracking + self.use_multimask_token_for_obj_ptr = use_multimask_token_for_obj_ptr + self.use_best_iou_mask_for_mem_enc = use_best_iou_mask_for_mem_enc + self.iou_prediction_use_sigmoid = iou_prediction_use_sigmoid + self.object_score_logit_threshold = object_score_logit_threshold + self.stability_score_attentuation = stability_score_attentuation + if iter_use_prev_mask_pred: + # In this case, we are feeding the previously predicted SAM mask logits + # as mask prompt into the SAM mask decoder, which has a different format + # and magnitude from GT mask input in VOS. Therefore in this case, the GT + # mask input must be encoded directly (not through the SAM mask decoder). + if min(prob_to_use_pt_input_for_train, prob_to_use_pt_input_for_eval) < 1: + assert use_mask_input_as_output_without_sam + self.iter_use_prev_mask_pred = iter_use_prev_mask_pred + + # Part 4: SAM-style prompt encoder (for both mask and point inputs) + # and SAM-style mask decoder for the final mask output + self.image_size = image_size + self.backbone_stride = backbone_stride + self.low_res_mask_size = self.image_size // self.backbone_stride * 4 + # we resize the mask if it doesn't match `self.input_mask_size` (which is always 4x + # the low-res mask size, regardless of the actual input image size); this is because + # `_use_mask_as_output` always downsamples the input masks by 4x + self.input_mask_size = self.low_res_mask_size * 4 + self.forward_backbone_per_frame_for_eval = forward_backbone_per_frame_for_eval + self.offload_output_to_cpu_for_eval = offload_output_to_cpu_for_eval + if trim_past_non_cond_mem_for_eval: + assert ( + num_frames_to_correct_for_eval <= 1 + ), "trim_past_non_cond_mem_for_eval=True requires that only the first frame receives prompts" + self.trim_past_non_cond_mem_for_eval = trim_past_non_cond_mem_for_eval + self.sam_mask_decoder_extra_args = sam_mask_decoder_extra_args + self.interactive_sam_mask_decoder_extra_args = ( + interactive_sam_mask_decoder_extra_args + ) + self.pred_obj_scores = pred_obj_scores + self.pred_obj_scores_mlp = pred_obj_scores_mlp + self.fixed_no_obj_ptr = fixed_no_obj_ptr + self.use_no_obj_ptr = use_no_obj_ptr + self.use_linear_no_obj_ptr = use_linear_no_obj_ptr + + if self.fixed_no_obj_ptr: + assert self.pred_obj_scores + assert self.use_obj_ptrs_in_encoder + if ( + self.pred_obj_scores + and self.use_obj_ptrs_in_encoder + and self.use_no_obj_ptr + ): + if self.use_linear_no_obj_ptr: + self.no_obj_ptr_linear = nn.Linear(self.hidden_dim, self.hidden_dim) + else: + self.no_obj_ptr = torch.nn.Parameter( + torch.zeros(self.multiplex_count, self.hidden_dim) + ) + trunc_normal_(self.no_obj_ptr, std=0.02) + + self.use_mlp_for_obj_ptr_proj = use_mlp_for_obj_ptr_proj + self.no_obj_embed_spatial = None + if no_obj_embed_spatial: + self.no_obj_embed_spatial = torch.nn.Parameter( + torch.zeros(self.multiplex_count, self.hidden_dim) + ) + trunc_normal_(self.no_obj_embed_spatial, std=0.02) + self.num_multimask_outputs = num_multimask_outputs + self.decode_mask_with_shared_tokens = decode_mask_with_shared_tokens + self.decode_mask_attribute_with_shared_tokens = ( + decode_mask_attribute_with_shared_tokens + ) + self.share_necks = share_necks + + self.add_output_suppression_embeddings = add_output_suppression_embeddings + if self.add_output_suppression_embeddings: + self.output_valid_embed = torch.nn.Parameter( + torch.zeros(self.multiplex_count, self.hidden_dim) + ) + self.output_invalid_embed = torch.nn.Parameter( + torch.zeros(self.multiplex_count, self.hidden_dim) + ) + trunc_normal_(self.output_valid_embed, std=0.02) + trunc_normal_(self.output_invalid_embed, std=0.02) + self.add_object_conditional_embeddings = add_object_conditional_embeddings + if add_object_unconditional_embeddings is None: + add_object_unconditional_embeddings = add_object_conditional_embeddings + self.add_object_unconditional_embeddings = add_object_unconditional_embeddings + if add_object_unconditional_embeddings: + assert add_object_conditional_embeddings + if self.add_object_conditional_embeddings: + # have embeddings for both conditional and non-conditional objects + # such that the features are more "balanced" + # these three sets should be disjoint and their union should cover all objects + # for conditioning objects + self.obj_cond_embed = torch.nn.Parameter( + torch.zeros(self.multiplex_count, self.hidden_dim) + ) + trunc_normal_(self.obj_cond_embed, std=0.02) + if self.add_object_unconditional_embeddings: + # for non-conditioning objects + self.obj_non_cond_embed = torch.nn.Parameter( + torch.zeros(self.multiplex_count, self.hidden_dim) + ) + trunc_normal_(self.obj_non_cond_embed, std=0.02) + + self.condition_as_mask_input = condition_as_mask_input + self.condition_as_mask_input_fg = condition_as_mask_input_fg + self.condition_as_mask_input_bg = condition_as_mask_input_bg + + self.is_dynamic_model = is_dynamic_model + + self._build_sam_heads() + + # Point sampler and conditioning frames + self.prob_to_use_pt_input_for_train = prob_to_use_pt_input_for_train + self.prob_to_use_box_input_for_train = prob_to_use_box_input_for_train + self.prob_to_use_pt_input_for_eval = prob_to_use_pt_input_for_eval + self.prob_to_use_box_input_for_eval = prob_to_use_box_input_for_eval + if prob_to_use_pt_input_for_train > 0 or prob_to_use_pt_input_for_eval > 0: + logging.info("Using points (sampled from masks) as inputs") + assert num_frames_to_correct_for_train >= num_init_cond_frames_for_train + assert num_frames_to_correct_for_eval >= num_init_cond_frames_for_eval + self.num_frames_to_correct_for_train = num_frames_to_correct_for_train + self.num_frames_to_correct_for_eval = num_frames_to_correct_for_eval + self.rand_frames_to_correct_for_train = rand_frames_to_correct_for_train + self.rand_frames_to_correct_for_eval = rand_frames_to_correct_for_eval + self.prob_correct_all_objects_for_train = prob_correct_all_objects_for_train + self.ratio_of_objects_to_correct_for_train = ( + ratio_of_objects_to_correct_for_train + ) + self.rand_objects_to_correct_for_train = rand_objects_to_correct_for_train + self.force_correct_all_for_conditional_inputs = ( + force_correct_all_for_conditional_inputs + ) + # Initial multi-conditioning frames + self.num_init_cond_frames_for_train = num_init_cond_frames_for_train + self.num_init_cond_frames_for_eval = num_init_cond_frames_for_eval + self.rand_init_cond_frames_for_train = rand_init_cond_frames_for_train + self.rand_init_cond_frames_for_eval = rand_init_cond_frames_for_eval + self.max_cond_frames_in_attn = max_cond_frames_in_attn + self.keep_first_cond_frame = keep_first_cond_frame + self.add_all_frames_to_correct_as_cond = add_all_frames_to_correct_as_cond + self.num_correction_pt_per_frame = num_correction_pt_per_frame + self.pt_sampling_for_eval = pt_sampling_for_eval + self.prob_to_sample_from_gt_for_train = prob_to_sample_from_gt_for_train + # A random number generator with a fixed initial seed across GPUs + self.rng = np.random.default_rng(seed=42) + if randomness_fix: + self.rng2 = np.random.default_rng(seed=42) + else: + self.rng2 = self.rng + + # Use frame filtering according to SAM2Long + self.use_memory_selection = use_memory_selection + self.mf_threshold = mf_threshold + + # Compile all components of the model + self.compile_all_components = compile_all_components + if self.compile_all_components: + self._compile_all_components() + + def _get_tpos_enc(self, rel_pos_list, device, max_abs_pos=None, dummy=False): + if dummy: + return torch.zeros(len(rel_pos_list), self.mem_dim, device=device) + + t_diff_max = max_abs_pos - 1 if max_abs_pos is not None else 1 + pos_enc = ( + torch.tensor(rel_pos_list).pin_memory().to(device=device, non_blocking=True) + / t_diff_max + ) + if self.sincos_tpos_enc: + tpos_dim = ( + self.hidden_dim if self.proj_tpos_enc_in_obj_ptrs else self.mem_dim + ) + pos_enc = get_1d_sine_pe(pos_enc, dim=tpos_dim) + else: + raise NotImplementedError + pos_enc = self.obj_ptr_tpos_proj(pos_enc) + + return pos_enc + + def _build_sam_heads(self): + """Build SAM-style prompt encoder and mask decoder.""" + self.sam_prompt_embed_dim = self.hidden_dim + self.sam_image_embedding_size = self.image_size // self.backbone_stride + + self.image_pe_layer = PositionEmbeddingRandom(self.hidden_dim // 2) + + # build PromptEncoder and MaskDecoder from SAM + # (their hyperparameters like `mask_in_chans=16` are from SAM code) + self.interactive_sam_prompt_encoder = PromptEncoder( + embed_dim=self.sam_prompt_embed_dim, + image_embedding_size=( + self.sam_image_embedding_size, + self.sam_image_embedding_size, + ), + input_image_size=(self.image_size, self.image_size), + mask_in_chans=16, + ) + + self.interactive_sam_mask_decoder = MaskDecoder( + num_multimask_outputs=3, + transformer=TwoWayTransformer( + depth=2, + embedding_dim=self.sam_prompt_embed_dim, + mlp_dim=2048, + num_heads=8, + ), + transformer_dim=self.sam_prompt_embed_dim, + iou_head_depth=3, + iou_head_hidden_dim=256, + use_high_res_features=self.use_high_res_features_in_sam, + iou_prediction_use_sigmoid=self.iou_prediction_use_sigmoid, + pred_obj_scores=self.pred_obj_scores, + pred_obj_scores_mlp=self.pred_obj_scores_mlp, + use_multimask_token_for_obj_ptr=self.use_multimask_token_for_obj_ptr, + **(self.interactive_sam_mask_decoder_extra_args or {}), + ) + if self.share_necks: + # we will use self.sam_mask_decoder's convs + del self.interactive_sam_mask_decoder.conv_s0 + del self.interactive_sam_mask_decoder.conv_s1 + + self.sam_mask_decoder = MultiplexMaskDecoder( + multiplex_count=self.multiplex_count, + num_multimask_outputs=self.num_multimask_outputs, + transformer=TwoWayTransformer( + depth=2, + embedding_dim=self.hidden_dim, + mlp_dim=2048, + num_heads=8, + ), + transformer_dim=self.hidden_dim, + iou_head_depth=3, + iou_head_hidden_dim=256, + use_high_res_features=self.use_high_res_features_in_sam, + iou_prediction_use_sigmoid=self.iou_prediction_use_sigmoid, + pred_obj_scores=self.pred_obj_scores, + pred_obj_scores_mlp=self.pred_obj_scores_mlp, + use_multimask_token_for_obj_ptr=self.use_multimask_token_for_obj_ptr, + decode_mask_with_shared_tokens=self.decode_mask_with_shared_tokens, + decode_mask_attribute_with_shared_tokens=self.decode_mask_attribute_with_shared_tokens, + multimask_outputs_only=self.num_multimask_outputs > 0 + and self.multimask_output_in_sam, + **(self.sam_mask_decoder_extra_args or {}), + ) + + if self.use_obj_ptrs_in_encoder: + # a linear projection on SAM output tokens to turn them into object pointers + self.obj_ptr_proj = torch.nn.Linear(self.hidden_dim, self.hidden_dim) + self.interactive_obj_ptr_proj = torch.nn.Linear( + self.hidden_dim, self.hidden_dim + ) + if self.use_mlp_for_obj_ptr_proj: + self.obj_ptr_proj = MLP( + self.hidden_dim, self.hidden_dim, self.hidden_dim, 3 + ) + self.interactive_obj_ptr_proj = MLP( + self.hidden_dim, self.hidden_dim, self.hidden_dim, 3 + ) + else: + self.obj_ptr_proj = torch.nn.Identity() + self.interactive_obj_ptr_proj = torch.nn.Identity() + if self.proj_tpos_enc_in_obj_ptrs: + # a linear projection on temporal positional encoding in object pointers to + # avoid potential interference with spatial positional encoding + self.obj_ptr_tpos_proj = torch.nn.Linear(self.hidden_dim, self.mem_dim) + else: + self.obj_ptr_tpos_proj = torch.nn.Identity() + + def _get_interactive_pix_mem( + self, features: torch.Tensor, feat_sizes: list[tuple] + ) -> torch.Tensor: + assert self.directly_add_no_mem_embed + pix_feat_with_mem = features[-1] + self.interactivity_no_mem_embed + B = features[-1].size(1) # batch size on this frame + C = self.hidden_dim + H, W = feat_sizes[-1] # top-level (lowest-resolution) feature size + pix_feat_with_mem = pix_feat_with_mem.permute(1, 2, 0).view(B, C, H, W) + return pix_feat_with_mem + + def _forward_sam_heads( + self, + backbone_features: torch.Tensor, + *, + point_inputs: Optional[dict[str, torch.Tensor]] = None, + mask_inputs: Optional[torch.Tensor] = None, + interactive_high_res_features: Optional[list[torch.Tensor]] = None, + propagation_high_res_features: Optional[list[torch.Tensor]] = None, + multimask_output: bool = False, + gt_masks=None, + multiplex_state: MultiplexState, + objects_to_interact: Optional[list[int]] = None, + ) -> SAMOutput: + """ + Forward SAM prompt encoders and mask heads. + We run the propagation head, the interactive head, or both, based on the inputs. + + Inputs: + - backbone_features: image features of [B, C, H, W] shape + - point_inputs: a dictionary with "point_coords" and "point_labels", where + 1) "point_coords" has [B, P, 2] shape and float32 dtype and contains the + absolute pixel-unit coordinate in (x, y) format of the P input points + 2) "point_labels" has shape [B, P] and int32 dtype, where 1 means + positive clicks, 0 means negative clicks, and -1 means padding + - mask_inputs: a mask of [B, 1, H*16, W*16] shape, float or bool, with the + same spatial size as the image. + - high_res_features: either 1) None or 2) a list of length 2 containing + two feature maps of [B, C, 4*H, 4*W] and [B, C, 2*H, 2*W] shapes respectively, + which will be used as high-resolution feature maps for SAM decoder. + - multimask_output: if it's True, we output 3 candidate masks and their 3 + corresponding IoU estimates, and if it's False, we output only 1 mask and + its corresponding IoU estimate. + + Outputs: + - low_res_multimasks: [B, M, H*4, W*4] shape (where M = 3 if + `multimask_output=True` and M = 1 if `multimask_output=False`), the SAM + output mask logits (before sigmoid) for the low-resolution masks, with 4x + the resolution (1/4 stride) of the input backbone_features. + - high_res_multimasks: [B, M, H*16, W*16] shape (where M = 3 + if `multimask_output=True` and M = 1 if `multimask_output=False`), + upsampled from the low-resolution masks, with shape size as the image + (stride is 1 pixel). + - ious: [B, M] shape (where M = 3 if `multimask_output=True` and M = 1 + if `multimask_output=False`), the estimated IoU of each output mask. + - low_res_masks: [B, 1, H*4, W*4] shape, the best mask in `low_res_multimasks`. + If `multimask_output=True`, it's the mask with the highest IoU estimate. + If `multimask_output=False`, it's the same as `low_res_multimasks`. + - high_res_masks: [B, 1, H*16, W*16] shape, the best mask in `high_res_multimasks`. + If `multimask_output=True`, it's the mask with the highest IoU estimate. + If `multimask_output=False`, it's the same as `high_res_multimasks`. + - obj_ptr: [num_buckets, multiplex_count, C] shape, the object pointer vector for + the output mask, extracted based on the output token from the SAM mask decoder. + """ + + device = backbone_features.device + assert backbone_features.size(1) == self.hidden_dim + assert backbone_features.size(2) == self.sam_image_embedding_size + assert backbone_features.size(3) == self.sam_image_embedding_size + + is_interactive = point_inputs is not None or mask_inputs is not None + + if is_interactive: + """ + Image-level, per-object interactive path + """ + assert interactive_high_res_features is not None + assert objects_to_interact is not None + + # a) Handle point prompts + if point_inputs is not None: + sam_point_coords = point_inputs["point_coords"] + sam_point_labels = point_inputs["point_labels"] + else: + assert mask_inputs is not None + # If no points are provided, pad with an empty point (with label -1) + sam_point_coords = torch.zeros( + mask_inputs.shape[0], 1, 2, device=device + ) + sam_point_labels = -torch.ones( + mask_inputs.shape[0], 1, dtype=torch.int32, device=device + ) + + # b) Handle mask prompts + if mask_inputs is not None: + # If mask_inputs is provided, downsize it into low-res mask input if needed + # and feed it as a dense mask prompt into the SAM mask encoder + assert len(mask_inputs.shape) == 4 + if ( + mask_inputs.shape[-2:] + != self.interactive_sam_prompt_encoder.mask_input_size + ): + sam_mask_prompt = F.interpolate( + mask_inputs.float(), + size=self.interactive_sam_prompt_encoder.mask_input_size, + align_corners=False, + mode="bilinear", + antialias=True, # use antialias for downsampling + ) + else: + sam_mask_prompt = mask_inputs + else: + # Otherwise, simply feed None (and SAM's prompt encoder will add + # a learned `no_mask_embed` to indicate no mask input in this case). + sam_mask_prompt = None + + sparse_embeddings, dense_embeddings = self.interactive_sam_prompt_encoder( + points=(sam_point_coords, sam_point_labels), + boxes=None, + masks=sam_mask_prompt, + ) + + # Clone image_pe and the outputs of sam_prompt_encoder + # to enable compilation + sparse_embeddings = self._maybe_clone(sparse_embeddings) + dense_embeddings = self._maybe_clone(dense_embeddings) + image_pe = self._maybe_clone( + self.interactive_sam_prompt_encoder.get_dense_pe() + ) + ( + low_res_multimasks, + ious, + sam_output_tokens, + object_score_logits, + ) = self.interactive_sam_mask_decoder( + image_embeddings=backbone_features, + image_pe=image_pe, + sparse_prompt_embeddings=sparse_embeddings, + dense_prompt_embeddings=dense_embeddings, + multimask_output=multimask_output, + repeat_image=True, + high_res_features=interactive_high_res_features, + ) + + else: + """ + Multiplexed propagation path + """ + assert propagation_high_res_features is not None + assert multiplex_state is not None + + if self.add_output_suppression_embeddings: + # the suppression embeddings inform the mask decoder the objects that should be decoded + output_valid_embed = self.output_valid_embed.unsqueeze(0) + output_invalid_embed = self.output_invalid_embed.unsqueeze(0) + valid_object_mask = ( + multiplex_state.get_valid_object_mask().unsqueeze(-1).float() + ) + output_merged_embed = ( + valid_object_mask * output_valid_embed + + (1 - valid_object_mask) * output_invalid_embed + ) + else: + output_merged_embed = None + + # Clone image_pe to enable compilation + image_pe = self._maybe_clone(self.get_propagation_dense_pe()) + out = self.sam_mask_decoder( + image_embeddings=backbone_features, + image_pe=image_pe, + high_res_features=propagation_high_res_features, + multimask_output=multimask_output, + extra_per_object_embeddings=output_merged_embed, + ) + low_res_multimasks = out["masks"] # [B, M, 3/1, H*4, W*4] + ious = out["iou_pred"] # [B, M, 3/1] + sam_output_tokens = out["sam_tokens_out"] # [B, M, 3/1, C] + object_score_logits = out["object_score_logits"] + + low_res_multimasks = multiplex_state.demux(low_res_multimasks) + ious = multiplex_state.demux(ious) + object_score_logits = multiplex_state.demux(object_score_logits) + sam_output_tokens = multiplex_state.demux(sam_output_tokens) + + """ + The interactive and the propagation paths converge here + """ + # Clone the output of sam_mask_decoder + # to enable compilation + low_res_multimasks = self._maybe_clone(low_res_multimasks) + ious = self._maybe_clone(ious) + object_score_logits = self._maybe_clone(object_score_logits) + sam_output_tokens = self._maybe_clone(sam_output_tokens) + + if self.pred_obj_scores: + is_obj_appearing = object_score_logits > self.object_score_logit_threshold + + # Mask used for spatial memories is always a *hard* choice between obj and no obj, + # consistent with the actual mask prediction + low_res_multimasks = torch.where( + is_obj_appearing[:, None, None], + low_res_multimasks, + NO_OBJ_SCORE, + ) + + # convert masks from possibly bfloat16 (or float16) to float32 + # (older PyTorch versions before 2.1 don't support `interpolate` on bf16) + low_res_multimasks = low_res_multimasks.float() + high_res_multimasks = F.interpolate( + low_res_multimasks, + size=(self.image_size, self.image_size), + mode="bilinear", + align_corners=False, + ) + + sam_output_token = sam_output_tokens[:, 0] + if multimask_output and ( + not self.decode_mask_with_shared_tokens or is_interactive + ): + # take the best mask prediction (with the highest IoU estimation) + if self.stability_score_attentuation: + # prefer selecting masks with high stability score + stability_score = self.sam_mask_decoder._get_stability_scores( + low_res_multimasks + ) + ious = ious * stability_score + + best_iou_inds = torch.argmax(ious, dim=-1) + batch_inds = torch.arange(ious.shape[0], device=device) + + low_res_masks = low_res_multimasks[batch_inds, best_iou_inds].unsqueeze(1) + high_res_masks = high_res_multimasks[batch_inds, best_iou_inds].unsqueeze(1) + if sam_output_tokens.size(1) > 1: + sam_output_token = sam_output_tokens[batch_inds, best_iou_inds] + else: + if multimask_output and not is_interactive: + assert self.decode_mask_with_shared_tokens + low_res_masks = low_res_multimasks[:, 0:1] + high_res_masks = high_res_multimasks[:, 0:1] + else: + low_res_masks = low_res_multimasks + high_res_masks = high_res_multimasks + + # Extract object pointer from the SAM output token + if self.use_obj_ptrs_in_encoder: + if is_interactive: + obj_ptr = self.interactive_obj_ptr_proj(sam_output_token) + else: + obj_ptr = self.obj_ptr_proj(sam_output_token) + + if self.pred_obj_scores and self.use_no_obj_ptr: + lambda_is_obj_appearing = is_obj_appearing.float() + if self.use_linear_no_obj_ptr: + obj_ptr = lambda_is_obj_appearing * obj_ptr + ( + 1 - lambda_is_obj_appearing + ) * self.no_obj_ptr_linear(obj_ptr) + else: + if self.fixed_no_obj_ptr: + obj_ptr = lambda_is_obj_appearing * obj_ptr + + # use demux to locate the corresponding no_obj_ptr entries + selected_no_obj_ptr = self.no_obj_ptr.unsqueeze(0).repeat( + multiplex_state.num_buckets, 1, 1 + ) + selected_no_obj_ptr = multiplex_state.demux(selected_no_obj_ptr) + if is_interactive: + # if is_interactive, the object pointers are in the data space + selected_no_obj_ptr = selected_no_obj_ptr[objects_to_interact] + + obj_ptr = ( + obj_ptr + (1 - lambda_is_obj_appearing) * selected_no_obj_ptr + ) + + outputs: SAMOutput = { + "low_res_multimasks": low_res_multimasks, + "high_res_multimasks": high_res_multimasks, + "ious": ious, + "low_res_masks": low_res_masks, + "high_res_masks": high_res_masks, + "object_score_logits": object_score_logits, + } + if self.use_obj_ptrs_in_encoder: + outputs["obj_ptr"] = obj_ptr # [num_objects, C], in data space + return outputs + + def _use_mask_as_output( + self, + backbone_features: torch.Tensor, + high_res_features: list[torch.Tensor], + mask_inputs: torch.Tensor, + multiplex_state: MultiplexState, + objects_in_mask: Optional[list[int]] = None, + ) -> SAMOutput: + """ + Directly turn binary `mask_inputs` into a output mask logits without using SAM. + (same input and output shapes as in _forward_sam_heads above). + """ + if objects_in_mask is None: + objects_in_mask = list(range(multiplex_state.total_valid_entries)) + + # Use -10/+10 as logits for neg/pos pixels (very close to 0/1 in prob after sigmoid). + out_scale, out_bias = 20.0, -10.0 # sigmoid(-10.0)=4.5398e-05 + mask_inputs_float = mask_inputs.to(backbone_features.dtype) + assert mask_inputs.shape[0] == len( + objects_in_mask + ), f"{mask_inputs.shape[0]} != {len(objects_in_mask)}" + high_res_masks = mask_inputs_float * out_scale + out_bias + low_res_masks = F.interpolate( + high_res_masks, + size=(high_res_masks.size(-2) // 4, high_res_masks.size(-1) // 4), + align_corners=False, + mode="bilinear", + antialias=True, # use antialias for downsampling + ) + # a dummy IoU prediction of all 1's under mask input + ious = mask_inputs.new_ones( + mask_inputs.size(0), 1, dtype=backbone_features.dtype + ) + + if self.use_obj_ptrs_in_encoder: + # produce an object pointer using the SAM decoder from the mask input + sam_outputs = self._forward_sam_heads( + backbone_features=backbone_features, + mask_inputs=self.interactive_mask_downsample(mask_inputs_float), + interactive_high_res_features=high_res_features, + gt_masks=mask_inputs, + objects_to_interact=objects_in_mask, + multiplex_state=multiplex_state, + ) + obj_ptr = sam_outputs["obj_ptr"] + + # In this method, we are treating mask_input as output, e.g. using it directly to create spatial mem; + # Below, we follow the same design axiom to use mask_input to decide if obj appears or not instead of relying + # on the object_scores from the SAM decoder. + is_obj_appearing = torch.any(mask_inputs.flatten(1).float() > 0.0, dim=1) + is_obj_appearing = is_obj_appearing[..., None] + lambda_is_obj_appearing = is_obj_appearing.float() + object_score_logits = out_scale * lambda_is_obj_appearing + out_bias + # Note that although this logic has already been applied in _forward_sam_heads + # it is ok because lambda_is_obj_appearing is binary + # when it is zero it forces no_obj_ptr + # when it is one it keeps the output from _forward_sam_heads + if self.pred_obj_scores and self.use_no_obj_ptr: + if self.use_linear_no_obj_ptr: + obj_ptr = lambda_is_obj_appearing * obj_ptr + ( + 1 - lambda_is_obj_appearing + ) * self.no_obj_ptr_linear(obj_ptr) + else: + if self.fixed_no_obj_ptr: + obj_ptr = lambda_is_obj_appearing * obj_ptr + # use demux to locate the corresponding no_obj_ptr entries + selected_no_obj_ptr = self.no_obj_ptr.unsqueeze(0).repeat( + multiplex_state.num_buckets, 1, 1 + ) + selected_no_obj_ptr = multiplex_state.demux(selected_no_obj_ptr) + selected_no_obj_ptr = selected_no_obj_ptr[objects_in_mask] + obj_ptr = ( + obj_ptr + (1 - lambda_is_obj_appearing) * selected_no_obj_ptr + ) + + outputs: SAMOutput = { + "low_res_multimasks": low_res_masks, + "high_res_multimasks": high_res_masks, + "ious": ious, + "low_res_masks": low_res_masks, + "high_res_masks": high_res_masks, + "object_score_logits": object_score_logits, + } + if self.use_obj_ptrs_in_encoder: + outputs["obj_ptr"] = obj_ptr # [num_objects, C], in data space + return outputs + + def forward(self, input: BatchedDatapoint, is_inference=False): + if self.training or not self.forward_backbone_per_frame_for_eval: + # precompute image features on all frames before tracking + backbone_out = self.forward_image( + input.img_batch, need_interactive_out=True, need_propagation_out=True + ) + else: + # defer image feature computation on a frame until it's being tracked + backbone_out = {} + backbone_out = self.prepare_prompt_inputs(backbone_out, input) + previous_stages_out = self.forward_tracking(backbone_out, input) + + # "None" for get_queries to be compatible with the trainer + return previous_stages_out, None + + def forward_image( + self, + img_batch, + *, + need_sam3_out: bool = False, + need_interactive_out: bool = False, + need_propagation_out: bool = False, + ): + """Get the image feature on the input batch.""" + if self.share_necks: + need_propagation_out = need_interactive_out or need_propagation_out + need_interactive_out = False + # this also means that convs for backbone_fpn are shared + backbone_out = self.backbone.forward_image( + img_batch, + need_sam3_out=need_sam3_out, + need_sam2_out=need_propagation_out, + ) + backbone_out["interactive"] = backbone_out["sam2_backbone_out"] + else: + backbone_out = self.backbone.forward_image( + img_batch, + need_sam3_out=need_sam3_out, + need_interactive_out=need_interactive_out, + need_propagation_out=need_propagation_out, + ) + if self.use_high_res_features_in_sam: + # precompute projected level 0 and level 1 features in SAM decoder + # to avoid running it again on every SAM click + if need_interactive_out: + backbone_out["interactive"]["backbone_fpn"][ + 0 + ].tensors = self.interactive_sam_mask_decoder.conv_s0( + backbone_out["interactive"]["backbone_fpn"][0].tensors + ) + backbone_out["interactive"]["backbone_fpn"][ + 1 + ].tensors = self.interactive_sam_mask_decoder.conv_s1( + backbone_out["interactive"]["backbone_fpn"][1].tensors + ) + if need_propagation_out: + backbone_out["sam2_backbone_out"]["backbone_fpn"][ + 0 + ].tensors = self.sam_mask_decoder.conv_s0( + backbone_out["sam2_backbone_out"]["backbone_fpn"][0].tensors + ) + backbone_out["sam2_backbone_out"]["backbone_fpn"][ + 1 + ].tensors = self.sam_mask_decoder.conv_s1( + backbone_out["sam2_backbone_out"]["backbone_fpn"][1].tensors + ) + # Clone to help torch.compile + for out_type in backbone_out.keys(): + for i in range(len(backbone_out[out_type]["backbone_fpn"])): + backbone_out[out_type]["backbone_fpn"][i].tensors = self._maybe_clone( + backbone_out[out_type]["backbone_fpn"][i].tensors + ) + backbone_out[out_type]["vision_pos_enc"][i] = self._maybe_clone( + backbone_out[out_type]["vision_pos_enc"][i] + ) + return backbone_out + + def _prepare_prompt_inputs_meta(self, backbone_out, input, start_frame_idx=0): + # Load the ground-truth masks on all frames (so that we can later + # sample correction points from them) + gt_masks_per_frame = { + stage_id: targets.segments.unsqueeze(1) # [B, 1, H_im, W_im] + for stage_id, targets in enumerate(input.find_targets) + } + backbone_out["gt_masks_per_frame"] = gt_masks_per_frame + num_frames = len(input.find_targets) + backbone_out["num_frames"] = num_frames + + # Randomly decide whether to use point inputs or mask inputs + if self.training: + prob_to_use_pt_input = self.prob_to_use_pt_input_for_train + num_frames_to_correct = self.num_frames_to_correct_for_train + rand_frames_to_correct = self.rand_frames_to_correct_for_train + num_init_cond_frames = self.num_init_cond_frames_for_train + rand_init_cond_frames = self.rand_init_cond_frames_for_train + else: + prob_to_use_pt_input = self.prob_to_use_pt_input_for_eval + num_frames_to_correct = self.num_frames_to_correct_for_eval + rand_frames_to_correct = self.rand_frames_to_correct_for_eval + num_init_cond_frames = self.num_init_cond_frames_for_eval + rand_init_cond_frames = self.rand_init_cond_frames_for_eval + if num_frames == 1: + # here we handle a special case for mixing video + SAM on image training, + # where we force using point input for the SAM task on static images + prob_to_use_pt_input = 1.0 + num_frames_to_correct = 1 + num_init_cond_frames = 1 + assert num_init_cond_frames >= 1 + # (here `self.rng.random()` returns value in range 0.0 <= X < 1.0) + use_pt_input = self.rng.random() < prob_to_use_pt_input + if rand_init_cond_frames and num_init_cond_frames > 1: + # randomly select 1 to `num_init_cond_frames` frames as initial conditioning frames + num_init_cond_frames = self.rng.integers( + 1, num_init_cond_frames, endpoint=True + ) + if ( + use_pt_input + and rand_frames_to_correct + and num_frames_to_correct > num_init_cond_frames + ): + # randomly select `num_init_cond_frames` to `num_frames_to_correct` frames to sample + # correction clicks (only for the case of point input) + num_frames_to_correct = self.rng.integers( + num_init_cond_frames, num_frames_to_correct, endpoint=True + ) + backbone_out["use_pt_input"] = use_pt_input + + # Sample initial conditioning frames + if num_init_cond_frames == 1: + init_cond_frames = [start_frame_idx] # starting frame + else: + # starting frame + randomly selected remaining frames (without replacement) + init_cond_frames = [start_frame_idx] + self.rng.choice( + range(start_frame_idx + 1, num_frames), + num_init_cond_frames - 1, + replace=False, + ).tolist() + backbone_out["init_cond_frames"] = init_cond_frames + backbone_out["frames_not_in_init_cond"] = [ + t for t in range(start_frame_idx, num_frames) if t not in init_cond_frames + ] + + # Sample frames where we will add correction clicks on the fly + # based on the error between prediction and ground-truth masks + if not use_pt_input: + # no correction points will be sampled when using mask inputs + frames_to_add_correction_pt = [] + elif num_frames_to_correct == num_init_cond_frames: + frames_to_add_correction_pt = init_cond_frames + else: + assert num_frames_to_correct > num_init_cond_frames + # initial cond frame + randomly selected remaining frames (without replacement) + extra_num = num_frames_to_correct - num_init_cond_frames + frames_to_add_correction_pt = ( + init_cond_frames + + self.rng.choice( + backbone_out["frames_not_in_init_cond"], extra_num, replace=False + ).tolist() + ) + backbone_out["frames_to_add_correction_pt"] = frames_to_add_correction_pt + + return backbone_out + + def _prepare_conditional_frames(self, backbone_out): + init_cond_frames = backbone_out["init_cond_frames"] + gt_masks_per_frame = backbone_out["gt_masks_per_frame"] + use_pt_input = backbone_out["use_pt_input"] + + if self.training: + prob_to_use_box_input = self.prob_to_use_box_input_for_train + else: + prob_to_use_box_input = self.prob_to_use_box_input_for_eval + + # Prepare mask or point inputs on initial conditioning frames + backbone_out["mask_inputs_per_frame"] = {} # {frame_idx: } + backbone_out["point_inputs_per_frame"] = {} # {frame_idx: } + for t in init_cond_frames: + if not use_pt_input: + backbone_out["mask_inputs_per_frame"][t] = gt_masks_per_frame[t] + else: + # During training # P(box) = prob_to_use_pt_input * prob_to_use_box_input + use_box_input = self.rng.random() < prob_to_use_box_input + if use_box_input: + points, labels = sample_box_points( + gt_masks_per_frame[t], + ) + else: + # (here we only sample **one initial point** on initial conditioning frames from the + # ground-truth mask; we may sample more correction points on the fly) + points, labels = get_next_point( + gt_masks=gt_masks_per_frame[t], + pred_masks=None, + method=( + "uniform" if self.training else self.pt_sampling_for_eval + ), + ) + + point_inputs = {"point_coords": points, "point_labels": labels} + backbone_out["point_inputs_per_frame"][t] = point_inputs + + return backbone_out + + def prepare_prompt_inputs(self, backbone_out, input, start_frame_idx=0): + """ + Prepare input mask, point or box prompts. Optionally, we allow tracking from + a custom `start_frame_idx` to the end of the video (for evaluation purposes). + """ + backbone_out = self._prepare_prompt_inputs_meta( + backbone_out, input, start_frame_idx + ) + backbone_out = self._prepare_conditional_frames(backbone_out) + return backbone_out + + def _prepare_backbone_features(self, backbone_out): + """Prepare and flatten visual features (same as in MDETR_API model).""" + + backbone_features = {} + + for neck_k in neck_outs: + if neck_k not in backbone_out: + continue + neck_out = backbone_out[neck_k] + assert len(neck_out["backbone_fpn"]) == len(neck_out["vision_pos_enc"]) + assert len(neck_out["backbone_fpn"]) >= self.num_feature_levels + + feature_maps = neck_out["backbone_fpn"][-self.num_feature_levels :] + vision_pos_embeds = neck_out["vision_pos_enc"][-self.num_feature_levels :] + + feat_sizes = [(x.shape[-2], x.shape[-1]) for x in vision_pos_embeds] + # flatten NxCxHxW to HWxNxC + vision_feats = [x.tensors.flatten(2).permute(2, 0, 1) for x in feature_maps] + vision_pos_embeds = [ + x.flatten(2).permute(2, 0, 1) for x in vision_pos_embeds + ] + vision_masks = [x.mask for x in feature_maps] + + for i, vision_mask in enumerate(vision_masks): + if vision_mask is not None: + vision_masks[i] = vision_mask.flatten(1) + + backbone_features[neck_k] = { + "vision_feats": vision_feats, + "vision_pos_embeds": vision_pos_embeds, + "vision_masks": vision_masks, + "feat_sizes": feat_sizes, + } + + return backbone_features + + def _prepare_backbone_features_per_frame( + self, + img_batch, + img_ids, + *, + need_interactive_out: bool = False, + need_propagation_out: bool = False, + ): + """Compute the image backbone features on the fly for the given img_ids.""" + # all image ids should be the same + assert img_ids.numel() == 1 + unique_img_ids = img_ids + + # Compute the image features on those unique image ids + image = img_batch.tensors[unique_img_ids] + image_mask = ( + img_batch.mask[unique_img_ids] if img_batch.mask is not None else None + ) + + backbone_out = self.forward_image( + NestedTensor(tensors=image, mask=image_mask), + need_interactive_out=need_interactive_out, + need_propagation_out=need_propagation_out, + ) + + backbone_features = self._prepare_backbone_features(backbone_out) + return image, backbone_features + + def _prepare_memory_conditioned_features( + self, + *, + frame_idx, + is_init_cond_frame, + current_vision_feats, + current_vision_masks, + current_vision_pos_embeds, + feat_sizes, + output_dict, + num_frames, + track_in_reverse=False, # tracking in reverse time order (for demo usage) + use_prev_mem_frame=True, # whether to condition on previous memory frames + multiplex_state: MultiplexState, + ): + """Fuse the current frame's visual feature map with previous memory.""" + B = multiplex_state.num_buckets + # B = current_vision_feats[-1].size(1) # batch size on this frame + vision_feat = current_vision_feats[-1].expand(-1, B, -1) + vision_mask = ( + current_vision_masks[-1].expand(-1, B, -1) + if current_vision_masks[-1] is not None + else None + ) + vision_pos_embed = current_vision_pos_embeds[-1].expand(-1, B, -1) + + C = self.hidden_dim + H, W = feat_sizes[-1] # top-level (lowest-resolution) feature size + device = current_vision_feats[-1].device + # The case of `self.num_maskmem == 0` below is primarily used for reproducing SAM on images. + # In this case, we skip the fusion with any memory. + if self.num_maskmem == 0: # Disable memory and skip fusion + pix_feat = vision_feat.permute(1, 2, 0).view(B, C, H, W) + return pix_feat + + num_obj_ptr_tokens = 0 + tpos_sign_mul = -1 if track_in_reverse else 1 + # Step 1: condition the visual features of the current frame on previous memories + if not is_init_cond_frame and use_prev_mem_frame: + # Retrieve the memories encoded with the maskmem backbone + # to_cat_prompt, to_cat_prompt_mask, to_cat_prompt_pos_embed = [], [], [] + to_cat_prompt, to_cat_prompt_pos_embed = [], [] + if self.save_image_features: + to_cat_image_feat, to_cat_image_pos_embed = [], [] + # Add conditioning frames's output first (all cond frames have t_pos=0 for + # when getting temporal positional embedding below) + assert len(output_dict["cond_frame_outputs"]) > 0 + # Select a maximum number of temporally closest cond frames for cross attention + cond_outputs = output_dict["cond_frame_outputs"] + selected_cond_outputs, unselected_cond_outputs = select_closest_cond_frames( + frame_idx, + cond_outputs, + self.max_cond_frames_in_attn, + keep_first_cond_frame=self.keep_first_cond_frame, + ) + + t_pos_and_prevs = [ + ((frame_idx - t) * tpos_sign_mul, out, True) + for t, out in selected_cond_outputs.items() + ] + # Add last (self.num_maskmem - 1) frames before current frame for non-conditioning memory + # the earliest one has t_pos=1 and the latest one has t_pos=self.num_maskmem-1 + # We also allow taking the memory frame non-consecutively (with r>1), in which case + # we take (self.num_maskmem - 2) frames among every r-th frames plus the last frame. + r = 1 if self.training else self.memory_temporal_stride_for_eval + + if self.use_memory_selection: + valid_indices = self.frame_filter( + output_dict, track_in_reverse, frame_idx, num_frames, r + ) + + for t_pos in range(1, self.num_maskmem): + t_rel = self.num_maskmem - t_pos # how many frames before current frame + if self.use_memory_selection: + if t_rel > len(valid_indices): + continue + prev_frame_idx = valid_indices[-t_rel] + else: + if t_rel == 1: + # for t_rel == 1, we take the last frame (regardless of r) + if not track_in_reverse: + # the frame immediately before this frame (i.e. frame_idx - 1) + prev_frame_idx = frame_idx - t_rel + else: + # the frame immediately after this frame (i.e. frame_idx + 1) + prev_frame_idx = frame_idx + t_rel + else: + # for t_rel >= 2, we take the memory frame from every r-th frames + if not track_in_reverse: + # first find the nearest frame among every r-th frames before this frame + # for r=1, this would be (frame_idx - 2) + prev_frame_idx = ((frame_idx - 2) // r) * r + # then seek further among every r-th frames + prev_frame_idx = prev_frame_idx - (t_rel - 2) * r + else: + # first find the nearest frame among every r-th frames after this frame + # for r=1, this would be (frame_idx + 2) + prev_frame_idx = -(-(frame_idx + 2) // r) * r + # then seek further among every r-th frames + prev_frame_idx = prev_frame_idx + (t_rel - 2) * r + out = output_dict["non_cond_frame_outputs"].get(prev_frame_idx, None) + if out is None: + # If an unselected conditioning frame is among the last (self.num_maskmem - 1) + # frames, we still attend to it as if it's a non-conditioning frame. + out = unselected_cond_outputs.get(prev_frame_idx, None) + t_pos_and_prevs.append((t_pos, out, False)) + + for t_pos, prev, is_selected_cond_frame in t_pos_and_prevs: + if prev is None: + continue # skip padding frames + + feats = prev.get("maskmem_features") + if feats is None: + continue + # "maskmem_features" might have been offloaded to CPU in demo use cases, + # so we load it back to GPU (it's a no-op if it's already on GPU). + feats = feats.cuda(non_blocking=True) + if feats.dim() == 5: + feats = multiplex_state.demux(feats).contiguous() + prev["maskmem_features"] = ( + feats.cpu() if not feats.is_cuda else feats + ) + + if feats.shape[0] == 0: + continue + + to_cat_prompt.append(feats.flatten(2).permute(2, 0, 1)) + # to_cat_prompt_mask.append(None) + # Spatial positional encoding (it might have been offloaded to CPU in eval) + maskmem_pos_list = prev.get("maskmem_pos_enc") + if not maskmem_pos_list: + continue + maskmem_enc = maskmem_pos_list[-1] + if maskmem_enc is None: + continue + maskmem_enc = maskmem_enc.cuda(non_blocking=True) + if maskmem_enc.dim() == 5: + maskmem_enc = multiplex_state.demux(maskmem_enc).contiguous() + prev["maskmem_pos_enc"][-1] = ( + maskmem_enc.cpu() if not maskmem_enc.is_cuda else maskmem_enc + ) + maskmem_enc = maskmem_enc.flatten(2).permute(2, 0, 1) + + if self.use_maskmem_tpos_v2: + # the last of maskmem_tpos_enc is an "out-of-range" embedding + if t_pos <= 0 or t_pos >= self.num_maskmem: + tpos_enc = self.maskmem_tpos_enc[self.num_maskmem - 1] + else: + tpos_enc = self.maskmem_tpos_enc[self.num_maskmem - t_pos - 1] + else: + # cond_frame NOT temporally encoded in this setting + # and last of the maskmem_tpos_enc is actually an + # indicator for being a cond_frame + t = t_pos if not is_selected_cond_frame else 0 + tpos_enc = self.maskmem_tpos_enc[self.num_maskmem - t - 1] + + maskmem_enc = maskmem_enc + tpos_enc + + if self.save_image_features: + # image features are in (HW)BC + image_feat = prev["image_features"].cuda() + image_pos_embed = prev["image_pos_enc"].cuda() + tpos_enc + to_cat_image_feat.append(image_feat) + to_cat_image_pos_embed.append(image_pos_embed) + + to_cat_prompt_pos_embed.append(maskmem_enc) + + # Construct the list of past object pointers + if self.use_obj_ptrs_in_encoder: + max_obj_ptrs_in_encoder = min(num_frames, self.max_obj_ptrs_in_encoder) + # First add those object pointers from selected conditioning frames + # (optionally, only include object pointers in the past during evaluation) + if not self.training and self.only_obj_ptrs_in_the_past_for_eval: + ptr_cond_outputs = { + t: out + for t, out in selected_cond_outputs.items() + if (t >= frame_idx if track_in_reverse else t <= frame_idx) + } + else: + ptr_cond_outputs = selected_cond_outputs + pos_and_outs_for_ptr = [ + # Temporal pos encoding contains how far away each pointer is from current frame + ( + ( + (frame_idx - t) * tpos_sign_mul + if self.use_signed_tpos_enc_to_obj_ptrs + else abs(frame_idx - t) + ), + out, + True, # is_selected_cond_frame + ) + for t, out in ptr_cond_outputs.items() + ] + + # Add up to (max_obj_ptrs_in_encoder - 1) non-conditioning frames before current frame + for t_diff in range(1, max_obj_ptrs_in_encoder): + if not self.use_memory_selection: + t = ( + frame_idx + t_diff + if track_in_reverse + else frame_idx - t_diff + ) + if t < 0 or (num_frames is not None and t >= num_frames): + break + else: + if -t_diff <= -len(valid_indices): + break + t = valid_indices[-t_diff] + + out = output_dict["non_cond_frame_outputs"].get( + t, unselected_cond_outputs.get(t, None) + ) + if out is not None: + pos_and_outs_for_ptr.append((t_diff, out, False)) + + # If we have at least one object pointer, add them to the across attention + if len(pos_and_outs_for_ptr) > 0: + pos_list, out_list, is_selected_cond_frame_list = zip( + *pos_and_outs_for_ptr + ) + # Filter out outputs that don't have obj_ptr (e.g., when object has empty mask) + filtered_data = [ + (pos, out, is_cond) + for pos, out, is_cond in zip( + pos_list, out_list, is_selected_cond_frame_list + ) + if "obj_ptr" in out + ] + + # Only proceed if we have at least one valid obj_ptr + if len(filtered_data) > 0: + pos_list, out_list, is_selected_cond_frame_list = zip( + *filtered_data + ) + # each out["obj_ptr"] is a tensor of shape (num_buckets, seq_len, C) + # cat object pointers along dim=0 into [ptr_seq_len, B, C] shape + obj_ptrs = torch.cat( + [out["obj_ptr"] for out in out_list], dim=1 + ).transpose(0, 1) + + # a temporal positional embedding based on how far each object pointer is from + # the current frame (sine embedding normalized by the max pointer num). + if self.add_tpos_enc_to_obj_ptrs: + obj_pos = self._get_tpos_enc( + pos_list, + max_abs_pos=max_obj_ptrs_in_encoder, + device=device, + ) + else: + obj_pos = self._get_tpos_enc( + pos_list, device=device, dummy=True + ) + # expand to batch size + obj_pos = obj_pos.unsqueeze(1).expand(-1, B, -1) + + assert ( + self.mem_dim == C + ), f"obj_ptrs.shape = {obj_ptrs.shape}, C = {C}" + + # each frame has [bucket_size] pointers, except the first frame + obj_pos = obj_pos.repeat_interleave( + multiplex_state.multiplex_count, dim=0 + ) + + to_cat_prompt.append(obj_ptrs) + to_cat_prompt_pos_embed.append(obj_pos) + # number of object pointer tokens for the encoder + num_obj_ptr_tokens = obj_ptrs.shape[0] + else: + # All outputs were filtered out (empty masks), no obj_ptrs available + num_obj_ptr_tokens = 0 + else: + num_obj_ptr_tokens = 0 + else: + # for initial conditioning frames, encode them without using any previous memory + raise NotImplementedError( + "Any init cond frame should have gone to _use_mask_as_output instead" + ) + + # Step 2: Concatenate the memories and forward through the transformer encoder + if len(to_cat_prompt) == 0: + # No available memory features (e.g. mask was cleared). Skip fusion and + # fall back to the current frame features so the object can continue to + # propagate as empty without raising errors. + pix_feat = vision_feat.permute(1, 2, 0).view(B, C, H, W) + return pix_feat + + prompt = torch.cat(to_cat_prompt, dim=0) + prompt_mask = None # For now, we always masks are zeros anyways + prompt_pos_embed = torch.cat(to_cat_prompt_pos_embed, dim=0) + + if self.save_image_features: + assert prompt_mask is None + assert vision_mask is None + if len(to_cat_image_feat) == 0 or len(to_cat_image_pos_embed) == 0: + # Memory image features were cleared; fall back to current-frame features. + pix_feat = vision_feat.permute(1, 2, 0).view(B, C, H, W) + return pix_feat + image_feat = torch.cat(to_cat_image_feat, dim=0) + image_pos_embed = torch.cat(to_cat_image_pos_embed, dim=0) + + encoder_out = self.transformer.encoder( + image=current_vision_feats[-1], + src=vision_feat, + memory_image=image_feat, + memory=prompt, + image_pos=current_vision_pos_embeds[-1], + src_pos=vision_pos_embed, + memory_image_pos=image_pos_embed, + memory_pos=prompt_pos_embed, + num_obj_ptr_tokens=num_obj_ptr_tokens, + ) + else: + encoder_out = self.transformer.encoder( + src=vision_feat, + src_key_padding_mask=vision_mask, + src_pos=vision_pos_embed, + prompt=prompt, + prompt_pos=prompt_pos_embed, + prompt_key_padding_mask=prompt_mask, + feat_sizes=feat_sizes, + num_obj_ptr_tokens=num_obj_ptr_tokens, + ) + # reshape the output (HW)BC => BCHW + pix_feat_with_mem = encoder_out["memory"].permute(1, 2, 0).view(B, C, H, W) + return pix_feat_with_mem + + def _encode_new_memory( + self, + image, + current_vision_feats, + feat_sizes, + pred_masks_high_res, + object_score_logits, + is_mask_from_pts, + *, + conditioning_objects: Optional[Iterable[int]] = None, + multiplex_state: MultiplexState, + ): + """Encode the current image and its prediction into a memory feature.""" + B = current_vision_feats[-1].size(1) # batch size on this frame + C = self.hidden_dim + H, W = feat_sizes[-1] # top-level (lowest-resolution) feature size + # top-level feature, (HW)BC => BCHW + pix_feat = current_vision_feats[-1].permute(1, 2, 0).view(B, C, H, W) + if self.non_overlap_masks_for_mem_enc and not self.training: + # optionally, apply non-overlapping constraints to the masks (it's applied + # in the batch dimension and should only be used during eval, where all + # the objects come from the same video under batch size 1). + pred_masks_high_res = self._apply_non_overlapping_constraints( + pred_masks_high_res + ) + if self.apply_sigmoid_to_mask_logits_for_mem_enc: + # scale the raw mask logits with a temperature before applying sigmoid + assert ( + not self.binarize_mask_from_pts_for_mem_enc + ), "haven't been trained this way; beware of hardcoded config override" + binarize = self.binarize_mask_from_pts_for_mem_enc and is_mask_from_pts + if binarize and not self.training: + mask_for_mem = (pred_masks_high_res > 0).float() + else: + # apply sigmoid on the raw mask logits to turn them into range (0, 1) + mask_for_mem = torch.sigmoid(pred_masks_high_res) + # apply scale and bias terms to the sigmoid probabilities + if self.sigmoid_scale_for_mem_enc != 1.0: + mask_for_mem = mask_for_mem * self.sigmoid_scale_for_mem_enc + if self.sigmoid_bias_for_mem_enc != 0.0: + mask_for_mem = mask_for_mem + self.sigmoid_bias_for_mem_enc + else: + mask_for_mem = pred_masks_high_res + + if self.add_object_conditional_embeddings or self.condition_as_mask_input: + # figure out the set of objects that are "conditional" on this frame + if conditioning_objects is None: + conditioning_objects = [] + unconditioning_objects = sorted( + list(multiplex_state.get_all_valid_object_idx()) + ) + else: + conditioning_objects = sorted(list(conditioning_objects)) + all_objects_idx = multiplex_state.get_all_valid_object_idx() + unconditioning_objects = sorted( + [i for i in all_objects_idx if i not in conditioning_objects] + ) + + mux_mask_for_mem = multiplex_state.mux(mask_for_mem).squeeze(2) + + if self.condition_as_mask_input: + # create num_objects channels spatial features that encode the + # list of objects that are conditional with fg and bg values + num_objects = mask_for_mem.shape[0] + # Create a 1D conditioning mask on GPU and broadcast it + cond_values = torch.full( + (num_objects,), + self.condition_as_mask_input_bg, + device=mask_for_mem.device, + dtype=mask_for_mem.dtype, + ) + if len(conditioning_objects) > 0: + cond_values[conditioning_objects] = self.condition_as_mask_input_fg + # Broadcast to full spatial dimensions: [N] -> [N, 1, H, W] + embedded_conditions = cond_values.view(-1, 1, 1, 1).expand_as(mask_for_mem) + embedded_conditions = multiplex_state.mux(embedded_conditions).squeeze(2) + + mux_mask_for_mem = torch.cat([mux_mask_for_mem, embedded_conditions], dim=1) + + if isinstance(self.maskmem_backbone, SimpleMaskEncoder): + maskmem_out = self.maskmem_backbone( + pix_feat, + mux_mask_for_mem, + skip_mask_sigmoid=True, + ) + else: + maskmem_out = self.maskmem_backbone(image, pix_feat, mux_mask_for_mem) + # Clone the feats and pos_enc to enable compilation + maskmem_features = self._maybe_clone(maskmem_out["vision_features"]) + maskmem_pos_enc = [self._maybe_clone(m) for m in maskmem_out["vision_pos_enc"]] + + if self.no_obj_embed_spatial is not None: + # since maskmem_features are deeply detangled between objects + # we simply add a projected embedding for each empty object + # num_buckets * multiplex_count * C + no_obj_embed_spatial = self.no_obj_embed_spatial.unsqueeze(0).repeat( + multiplex_state.num_buckets, 1, 1 + ) + # Align object_score_logits length to multiplex expectations before mux + if object_score_logits is not None: + obj_expected = multiplex_state.total_valid_entries + obj_current = object_score_logits.shape[0] + if obj_current != obj_expected: + if obj_current < obj_expected: + pad_shape = (obj_expected - obj_current,) + tuple( + object_score_logits.shape[1:] + ) + obj_pad = object_score_logits.new_zeros(pad_shape) + object_score_logits = torch.cat( + [object_score_logits, obj_pad], dim=0 + ) + else: + object_score_logits = object_score_logits[:obj_expected] + object_score_logits = multiplex_state.mux(object_score_logits) + is_obj_appearing = ( + object_score_logits > self.object_score_logit_threshold + ).float() + + no_obj_embed = ((1 - is_obj_appearing) * no_obj_embed_spatial).sum(dim=1) + maskmem_features += no_obj_embed[..., None, None].expand_as( + maskmem_features + ) + + if self.add_object_conditional_embeddings: + # add object conditional embeddings to the maskmem_features + # num_buckets * multiplex_count * C + obj_cond_embed = self.obj_cond_embed.unsqueeze(0).repeat( + multiplex_state.num_buckets, 1, 1 + ) + obj_cond_embed = multiplex_state.demux(obj_cond_embed) + obj_merged_embed = obj_cond_embed + + if self.add_object_unconditional_embeddings: + obj_non_cond_embed = self.obj_non_cond_embed.unsqueeze(0).repeat( + multiplex_state.num_buckets, 1, 1 + ) + obj_non_cond_embed = multiplex_state.demux(obj_non_cond_embed) + if self.training: + obj_merged_embed = obj_merged_embed.clone() + obj_merged_embed[unconditioning_objects] = obj_non_cond_embed[ + unconditioning_objects + ] + + obj_merged_embed = multiplex_state.mux(obj_merged_embed).sum(dim=1) + maskmem_features = maskmem_features + obj_merged_embed[ + ..., None, None + ].expand_as(maskmem_features) + + if maskmem_features.dim() == 5: + maskmem_features = multiplex_state.demux(maskmem_features).contiguous() + + demuxed_pos_enc = [] + for pos_enc in maskmem_pos_enc: + pos_enc_clone = pos_enc + if pos_enc_clone is not None and pos_enc_clone.dim() == 5: + pos_enc_clone = multiplex_state.demux(pos_enc_clone).contiguous() + demuxed_pos_enc.append(pos_enc_clone) + maskmem_pos_enc = demuxed_pos_enc + + return maskmem_features, maskmem_pos_enc + + def forward_tracking( + self, + backbone_out, + input, + return_dict=False, + objects_to_interact: Optional[list[int]] = None, + ): + """Forward video tracking on each frame (and sample correction clicks).""" + img_feats_already_computed = ( + "interactive" in backbone_out or "sam2_backbone_out" in backbone_out + ) + if img_feats_already_computed: + # Prepare the backbone features + # - vision_feats and vision_pos_embeds are in (HW)BC format + # - vision_masks are in B(HW) format, dtype=bool (False is valid, True is padding) + backbone_features = self._prepare_backbone_features(backbone_out) + + # Starting the stage loop + num_frames = backbone_out["num_frames"] + init_cond_frames = backbone_out["init_cond_frames"] + frames_to_add_correction_pt = backbone_out["frames_to_add_correction_pt"] + # first process all the initial conditioning frames to encode them as memory, + # and then conditioning on them to track the remaining frames + processing_order = init_cond_frames + backbone_out["frames_not_in_init_cond"] + + cond_frame_outputs: dict[int, StageOutput] = {} + non_cond_frame_outputs: dict[int, StageOutput] = {} + output_dict = { + "cond_frame_outputs": cond_frame_outputs, + "non_cond_frame_outputs": non_cond_frame_outputs, + } + + multiplex_state = self.multiplex_controller.get_state( + backbone_out["gt_masks_per_frame"][0].shape[0], + device=backbone_out["gt_masks_per_frame"][0].device, + dtype=torch.float, + random=self.training, + ) + + for stage_id in processing_order: + # Get the image features for the current frames + img_ids = input.find_inputs[stage_id].img_ids + # the image ids are for the entire batch + assert all( + [img_id == img_ids[0] for img_id in img_ids] + ) # should be all the same + # force this to have a batch size of 1 + img_ids = torch.tensor( + [img_ids[0]], device=img_ids.device, dtype=img_ids.dtype + ) + + if img_feats_already_computed: + # Retrieve image features according to img_ids (if they are already computed). + current_image = input.img_batch.tensors[img_ids] + current_backbone_features = {} + for neck_k, neck_out in backbone_features.items(): + current_backbone_features[neck_k] = { + "vision_feats": [ + x[:, img_ids] for x in neck_out["vision_feats"] + ], + "vision_masks": [ + x[img_ids] if x is not None else None + for x in neck_out["vision_masks"] + ], + "vision_pos_embeds": [ + x[:, img_ids] for x in neck_out["vision_pos_embeds"] + ], + "feat_sizes": neck_out["feat_sizes"], + } + else: + # Otherwise, compute the image features on the fly for the given img_ids + # (this might be used for evaluation on long videos to avoid backbone OOM). + need_interactive_out = (stage_id in frames_to_add_correction_pt) or ( + stage_id in init_cond_frames + ) + (current_image, current_backbone_features) = ( + self._prepare_backbone_features_per_frame( + input.img_batch, + img_ids, + need_interactive_out=need_interactive_out, + need_propagation_out=True, + ) + ) + + # Get output masks based on this frame's prompts and previous memory + current_out = self.track_step( + frame_idx=stage_id, + is_init_cond_frame=stage_id in init_cond_frames, + backbone_features_interactive=current_backbone_features.get( + "interactive" + ), + backbone_features_propagation=current_backbone_features.get( + "sam2_backbone_out" + ), + image=current_image, + point_inputs=backbone_out["point_inputs_per_frame"].get(stage_id, None), + mask_inputs=backbone_out["mask_inputs_per_frame"].get(stage_id, None), + gt_masks=backbone_out["gt_masks_per_frame"].get(stage_id, None), + frames_to_add_correction_pt=frames_to_add_correction_pt, + output_dict=output_dict, + num_frames=num_frames, + multiplex_state=multiplex_state, + objects_to_interact=objects_to_interact, + ) + # Append the output, depending on whether it's a conditioning frame + add_output_as_cond_frame = stage_id in init_cond_frames or ( + self.add_all_frames_to_correct_as_cond + and stage_id in frames_to_add_correction_pt + ) + if add_output_as_cond_frame: + output_dict["cond_frame_outputs"][stage_id] = current_out + else: + output_dict["non_cond_frame_outputs"][stage_id] = current_out + + output_dict["multiplex_state"] = multiplex_state + + if return_dict: + return output_dict + # turn `output_dict` into a list for loss function + all_frame_outputs = {} + all_frame_outputs.update(output_dict["cond_frame_outputs"]) + all_frame_outputs.update(output_dict["non_cond_frame_outputs"]) + all_frame_outputs = [all_frame_outputs[t] for t in range(num_frames)] + # Make DDP happy with activation checkpointing by removing unused keys + all_frame_outputs = [ + {k: v for k, v in d.items() if k != "obj_ptr"} for d in all_frame_outputs + ] + + return all_frame_outputs + + def _track_step_aux( + self, + *, + frame_idx, + is_init_cond_frame, + backbone_features_interactive, + backbone_features_propagation, + image, + point_inputs, + mask_inputs, + gt_masks, + frames_to_add_correction_pt, + output_dict, + num_frames, + track_in_reverse=False, # tracking in reverse time order (for demo usage) + run_mem_encoder=True, + prev_sam_mask_logits=None, + multiplex_state: MultiplexState, + objects_to_interact: Optional[list[int]] = None, + need_aux_output: bool = False, + ) -> tuple[StageOutput, dict]: + """ + There are four different modes that track_step might enter, based on the inputs + 1. Mask-as-output. This is when mask_inputs is not None. + The input mask is returned directly. This case is for FA/VOS initialization. + 2. Propagation-only. This is when mask_inputs and point_inputs are empty. + We propagate masks using the memory only. This case is for VOS propagation. + 3. Interaction-only. This is when mask_inputs is None, point_inputs is not None, + and one of the followings is satisified: + a) prev_sam_mask_logits is not None. In this case, we refine prev_sam_mask_logits + with additional interactions, updating only the objects specified in objects_to_interact. + objects_to_interact must not be None. + This occurs when we refine the same frame with multiple point inputs iteratively. + b) prev_sam_mask_logits is None, and is_init_cond_frame is True. + This case is for initializing the first frame. All objects will have point inputs. + This mostly happens during training/interactive eval. + 4. Propagation-and-interaction. This is when mask_inputs is None, point_inputs is not None, + prev_sam_mask_logits is None, and objects_to_interact is not None. + This is when we are propagating to a new frame that has point inputs (from previous interactions). + This is more of an edge case that could happen in offline interactive eval. + We first propagate the mask to the current frame, and then perform interaction on the selected + objects. Finally, we replace the masks of the interacted objects in the propagated output + with the masks from the interaction output. + """ + current_out: StageOutput = { + "conditioning_objects": set(), + "point_inputs": point_inputs, + "mask_inputs": mask_inputs, + } + + mode = None + if mask_inputs is not None: + mode = "mask_as_output" + elif point_inputs is None: + mode = "propagation_only" + elif point_inputs is not None: + # Case 3a: Refining existing predictions + if prev_sam_mask_logits is not None: + assert ( + objects_to_interact is not None + ), "objects_to_interact must be specified when refining with prev_sam_mask_logits" + mode = "interaction_only" + # Case 3b: Initial conditioning frame + elif is_init_cond_frame: + mode = "interaction_only" + # Case 4: Propagation then interaction + elif objects_to_interact is not None and prev_sam_mask_logits is None: + assert not self.training + mode = "propagation_and_interaction" + + if mode is None: + raise ValueError( + f"Unable to determine tracking case. " + f"mask_inputs={mask_inputs is not None}, " + f"point_inputs={point_inputs is not None}, " + f"prev_sam_mask_logits={prev_sam_mask_logits is not None}, " + f"objects_to_interact={objects_to_interact}, " + f"is_init_cond_frame={is_init_cond_frame}" + ) + # partition the backbone features + interactive_high_res_features = interactive_vision_feats = None + interactive_feat_sizes = None + if backbone_features_interactive is not None: + interactive_vision_feats = backbone_features_interactive["vision_feats"] + interactive_feat_sizes = backbone_features_interactive["feat_sizes"] + + # High-resolution feature maps for the SAM head, reshape (HW)BC => BCHW + if len(interactive_vision_feats) > 1: + interactive_high_res_features = [ + x.permute(1, 2, 0).view(x.size(1), x.size(2), *s) + for x, s in zip( + interactive_vision_feats[:-1], interactive_feat_sizes[:-1] + ) + ] + else: + # cannot do point interaction without interactive features + assert mode not in ["interaction_only", "propagation_and_interaction"] + + propagation_high_res_features = propagation_vision_feats = None + propagation_vision_masks = None + propagation_vision_pos_embeds = propagation_feat_sizes = None + if backbone_features_propagation is not None: + propagation_vision_feats = backbone_features_propagation["vision_feats"] + propagation_vision_masks = backbone_features_propagation["vision_masks"] + propagation_vision_pos_embeds = backbone_features_propagation[ + "vision_pos_embeds" + ] + propagation_feat_sizes = backbone_features_propagation["feat_sizes"] + + # High-resolution feature maps for the SAM head, reshape (HW)BC => BCHW + if len(propagation_vision_feats) > 1: + propagation_high_res_features = [ + x.permute(1, 2, 0).view(x.size(1), x.size(2), *s) + for x, s in zip( + propagation_vision_feats[:-1], propagation_feat_sizes[:-1] + ) + ] + else: + # we can get away without propagation features if we are interacting and not encoding new memory + assert mode not in ["propagation_only", "propagation_and_interaction"] + assert not run_mem_encoder + + interactive_pix_feat = None + if mode == "mask_as_output": + # simple encoding + assert self.use_mask_input_as_output_without_sam + # pix_feat = interactive_vision_feats[-1].permute(1, 2, 0) + # pix_feat = pix_feat.view(-1, self.hidden_dim, *interactive_feat_sizes[-1]) + # use no_mem_embed here as well to better align first-frame mask input vs point input + interactive_pix_feat = self._get_interactive_pix_mem( + interactive_vision_feats, interactive_feat_sizes + ) + sam_outputs = self._use_mask_as_output( + backbone_features=interactive_pix_feat, + high_res_features=interactive_high_res_features, + mask_inputs=mask_inputs, + multiplex_state=multiplex_state, + ) + # all the objects are conditional here + current_out["conditioning_objects"].update(range(mask_inputs.shape[0])) + else: + # propagation, interaction, or both + propagation_out = None + if mode in ["propagation_only", "propagation_and_interaction"]: + # gather the memory + assert backbone_features_propagation is not None + assert propagation_vision_feats is not None + assert propagation_vision_masks is not None + assert propagation_vision_pos_embeds is not None + assert propagation_feat_sizes is not None + pix_feat_with_mem = self._prepare_memory_conditioned_features( + frame_idx=frame_idx, + is_init_cond_frame=is_init_cond_frame, + current_vision_feats=propagation_vision_feats[-1:], + current_vision_masks=propagation_vision_masks[-1:], + current_vision_pos_embeds=propagation_vision_pos_embeds[-1:], + feat_sizes=propagation_feat_sizes[-1:], + output_dict=output_dict, + num_frames=num_frames, + track_in_reverse=track_in_reverse, + multiplex_state=multiplex_state, + ) + + # propagate the mask + # this is the propagation step; do not consider point_inputs here + multimask_output = self._use_multimask( + is_init_cond_frame, point_inputs=None + ) + propagation_out = self._forward_sam_heads( + backbone_features=pix_feat_with_mem, + propagation_high_res_features=propagation_high_res_features, + multimask_output=multimask_output, + objects_to_interact=list( + range(multiplex_state.total_valid_entries) + ), + multiplex_state=multiplex_state, + ) + + interaction_out = None + if mode in ["interaction_only", "propagation_and_interaction"]: + assert backbone_features_interactive is not None + assert interactive_vision_feats is not None + assert interactive_feat_sizes is not None + interactive_pix_feat = self._get_interactive_pix_mem( + interactive_vision_feats, interactive_feat_sizes + ) + + # apply SAM-style segmentation head + # here we might feed previously predicted low-res SAM mask logits into the SAM mask decoder, + # e.g. in demo where such logits come from earlier interaction instead of correction sampling + # (in this case, the SAM mask decoder should have `self.iter_use_prev_mask_pred=True`, and + # any `mask_inputs` shouldn't reach here as they are sent to _use_mask_as_output instead) + assert mask_inputs is None and point_inputs is not None + if prev_sam_mask_logits is not None: + assert objects_to_interact is not None + assert self.iter_use_prev_mask_pred + assert mode != "propagation_and_interaction" + mask_inputs = prev_sam_mask_logits[objects_to_interact] + elif mode == "propagation_and_interaction": + # use propagated masks as mask input + assert objects_to_interact is not None + assert propagation_out is not None + mask_inputs = propagation_out["low_res_masks"][objects_to_interact] + + if objects_to_interact is not None: + assert point_inputs["point_coords"].shape[0] == len( + objects_to_interact + ) + assert point_inputs["point_labels"].shape[0] == len( + objects_to_interact + ) + + multimask_output = self._use_multimask( + is_init_cond_frame, point_inputs=point_inputs + ) + interaction_out = self._forward_sam_heads( + backbone_features=interactive_pix_feat, + point_inputs=point_inputs, + mask_inputs=mask_inputs, + interactive_high_res_features=interactive_high_res_features, + multimask_output=multimask_output, + objects_to_interact=( + objects_to_interact + if objects_to_interact is not None + else list(range(multiplex_state.total_valid_entries)) + ), + multiplex_state=multiplex_state, + ) + if objects_to_interact is None: + current_out["conditioning_objects"].update( + multiplex_state.get_all_valid_object_idx() + ) + else: + current_out["conditioning_objects"].update(objects_to_interact) + + if propagation_out is None and interaction_out is not None: + sam_outputs = interaction_out + elif interaction_out is None and propagation_out is not None: + sam_outputs = propagation_out + else: + # merge the output + assert propagation_out is not None and interaction_out is not None + keys_to_merge = [ + "low_res_multimasks", + "high_res_multimasks", + "low_res_masks", + "high_res_masks", + "ious", + "object_score_logits", + "obj_ptr", + ] + for k in keys_to_merge: + src = interaction_out[k] + dst = propagation_out[k] + # Align dtype for floating tensors before indexed assignment + if torch.is_tensor(src) and torch.is_tensor(dst): + if torch.is_floating_point(src) and src.dtype != dst.dtype: + src = src.to(dtype=dst.dtype) + propagation_out[k][objects_to_interact] = src + sam_outputs = propagation_out + + low_res_multimasks = sam_outputs["low_res_multimasks"] + high_res_multimasks = sam_outputs["high_res_multimasks"] + ious = sam_outputs["ious"] + low_res_masks = sam_outputs["low_res_masks"] + high_res_masks = sam_outputs["high_res_masks"] + object_score_logits = sam_outputs["object_score_logits"] + + current_out["multistep_pred_masks"] = low_res_masks + current_out["multistep_pred_masks_high_res"] = high_res_masks + current_out["multistep_pred_multimasks"] = [low_res_multimasks] + current_out["multistep_pred_multimasks_high_res"] = [high_res_multimasks] + current_out["multistep_pred_ious"] = [ious] + current_out["multistep_point_inputs"] = [point_inputs] + current_out["multistep_object_score_logits"] = [object_score_logits] + + if self.use_obj_ptrs_in_encoder: + obj_ptr = sam_outputs["obj_ptr"] + + # Optionally, sample correction points iteratively to correct the mask + if frame_idx in frames_to_add_correction_pt: + assert gt_masks is not None + assert interactive_vision_feats is not None + assert interactive_feat_sizes is not None + all_pred_masks = [low_res_masks] + all_pred_high_res_masks = [high_res_masks] + all_pred_multimasks = [low_res_multimasks] + all_pred_high_res_multimasks = [high_res_multimasks] + all_pred_ious = [ious] + all_point_inputs = [point_inputs] + all_object_score_logits = [object_score_logits] + + # select a subset of objects to interact with + if self.training: + assert objects_to_interact is None + + interact_with_all_objects = ( + self.rng.random() < self.prob_correct_all_objects_for_train + ) or ( + self.force_correct_all_for_conditional_inputs and is_init_cond_frame + ) + + if interact_with_all_objects: + num_objects_to_correct = gt_masks.shape[0] + elif self.rand_objects_to_correct_for_train: + num_objects_to_correct = self.rng2.integers( + 1, + int( + gt_masks.shape[0] + * self.ratio_of_objects_to_correct_for_train + ) + + 1, + ) + else: + num_objects_to_correct = max( + 1, + int( + gt_masks.shape[0] + * self.ratio_of_objects_to_correct_for_train + ), + ) + + objects_to_interact = self.rng2.choice( + range(gt_masks.shape[0]), + size=num_objects_to_correct, + replace=False, + ).tolist() + + if point_inputs is not None: + # don't modify the point inputs in-place + point_inputs = { + "point_coords": point_inputs["point_coords"][ + objects_to_interact + ], + "point_labels": point_inputs["point_labels"][ + objects_to_interact + ], + } + else: + assert objects_to_interact is not None + # the point inputs should have been preselected, i.e., the following assertion should hold + + if point_inputs is not None: + assert point_inputs["point_coords"].shape[0] == len(objects_to_interact) + assert point_inputs["point_labels"].shape[0] == len(objects_to_interact) + + for _ in range(self.num_correction_pt_per_frame): + # sample a new point from the error between prediction and ground-truth + # (with a small probability, directly sample from GT masks instead of errors) + if self.training and self.prob_to_sample_from_gt_for_train > 0: + sample_from_gt = ( + self.rng.random() < self.prob_to_sample_from_gt_for_train + ) + else: + sample_from_gt = False + # if `pred_for_new_pt` is None, only GT masks will be used for point sampling + pred_for_new_pt = None if sample_from_gt else (high_res_masks > 0) + new_points, new_labels = get_next_point( + gt_masks=gt_masks[objects_to_interact], + pred_masks=( + pred_for_new_pt[objects_to_interact] + if pred_for_new_pt is not None + else None + ), + method="uniform" if self.training else self.pt_sampling_for_eval, + ) + point_inputs = concat_points(point_inputs, new_points, new_labels) + assert low_res_masks.shape[0] > max( + objects_to_interact + ), f"interacting {objects_to_interact} in {low_res_masks.shape}?" + if self.iter_use_prev_mask_pred: + # Feed the mask logits of the previous SAM outputs in the next SAM decoder step. + # For tracking, this means that when the user adds a correction click, we also feed + # the tracking output mask logits along with the click as input to the SAM decoder. + mask_inputs = low_res_masks[objects_to_interact] + multimask_output = self._use_multimask(is_init_cond_frame, point_inputs) + pix_feat_with_mem = self._get_interactive_pix_mem( + interactive_vision_feats, interactive_feat_sizes + ) + sam_outputs = self._forward_sam_heads( + backbone_features=pix_feat_with_mem, + point_inputs=point_inputs, + mask_inputs=mask_inputs, + interactive_high_res_features=interactive_high_res_features, + propagation_high_res_features=propagation_high_res_features, + multimask_output=multimask_output, + gt_masks=gt_masks, + objects_to_interact=objects_to_interact, + multiplex_state=multiplex_state, + ) + interact_low_res_multimasks = sam_outputs["low_res_multimasks"] + interact_high_res_multimasks = sam_outputs["high_res_multimasks"] + interact_ious = sam_outputs["ious"] + interact_low_res_masks = sam_outputs["low_res_masks"] + interact_high_res_masks = sam_outputs["high_res_masks"] + interact_object_score_logits = sam_outputs["object_score_logits"] + if self.use_obj_ptrs_in_encoder: + interact_obj_ptr = sam_outputs["obj_ptr"] + + if self.training: + # combine the masks from the interacted and non-interacted objects + low_res_masks = low_res_masks.clone() + high_res_masks = high_res_masks.clone() + low_res_multimasks = low_res_multimasks.clone() + high_res_multimasks = high_res_multimasks.clone() + ious = ious.clone() + object_score_logits = object_score_logits.clone() + obj_ptr = obj_ptr.clone() if self.use_obj_ptrs_in_encoder else None + + # Update masks for the interacted objects + if ( + torch.is_floating_point(interact_low_res_masks) + and interact_low_res_masks.dtype != low_res_masks.dtype + ): + interact_low_res_masks = interact_low_res_masks.to( + dtype=low_res_masks.dtype + ) + low_res_masks[objects_to_interact] = interact_low_res_masks + if ( + torch.is_floating_point(interact_high_res_masks) + and interact_high_res_masks.dtype != high_res_masks.dtype + ): + interact_high_res_masks = interact_high_res_masks.to( + dtype=high_res_masks.dtype + ) + high_res_masks[objects_to_interact] = interact_high_res_masks + if ( + torch.is_floating_point(interact_low_res_multimasks) + and interact_low_res_multimasks.dtype != low_res_multimasks.dtype + ): + interact_low_res_multimasks = interact_low_res_multimasks.to( + dtype=low_res_multimasks.dtype + ) + low_res_multimasks[objects_to_interact] = interact_low_res_multimasks + if ( + torch.is_floating_point(interact_high_res_multimasks) + and interact_high_res_multimasks.dtype != high_res_multimasks.dtype + ): + interact_high_res_multimasks = interact_high_res_multimasks.to( + dtype=high_res_multimasks.dtype + ) + high_res_multimasks[objects_to_interact] = interact_high_res_multimasks + if ( + torch.is_floating_point(interact_ious) + and interact_ious.dtype != ious.dtype + ): + interact_ious = interact_ious.to(dtype=ious.dtype) + ious[objects_to_interact] = interact_ious + if ( + torch.is_floating_point(interact_object_score_logits) + and interact_object_score_logits.dtype != object_score_logits.dtype + ): + interact_object_score_logits = interact_object_score_logits.to( + dtype=object_score_logits.dtype + ) + object_score_logits[objects_to_interact] = interact_object_score_logits + if self.use_obj_ptrs_in_encoder: + obj_ptr[objects_to_interact] = interact_obj_ptr + + all_pred_masks.append(low_res_masks) + all_pred_high_res_masks.append(high_res_masks) + all_pred_multimasks.append(low_res_multimasks) + all_pred_high_res_multimasks.append(high_res_multimasks) + all_pred_ious.append(ious) + all_point_inputs.append(point_inputs) + all_object_score_logits.append(object_score_logits) + + # Concatenate the masks along channel (to compute losses on all of them, + # using `onevision.losses.loss_fns.MultiStepIteractiveMasks`) + current_out["multistep_pred_masks"] = torch.cat(all_pred_masks, dim=1) + current_out["multistep_pred_masks_high_res"] = torch.cat( + all_pred_high_res_masks, dim=1 + ) + current_out["multistep_pred_multimasks"] = all_pred_multimasks + current_out["multistep_pred_multimasks_high_res"] = ( + all_pred_high_res_multimasks + ) + current_out["multistep_pred_ious"] = all_pred_ious + current_out["multistep_point_inputs"] = all_point_inputs + current_out["multistep_object_score_logits"] = all_object_score_logits + + if self.add_all_frames_to_correct_as_cond: + if objects_to_interact is None: + current_out["conditioning_objects"].update( + multiplex_state.get_all_valid_object_idx() + ) + else: + current_out["conditioning_objects"].update(set(objects_to_interact)) + + # Use the final prediction (after all correction steps for output and eval) + current_out["pred_masks"] = low_res_masks + current_out["pred_masks_high_res"] = high_res_masks + if self.use_obj_ptrs_in_encoder: + # similar to spatial memory, the object pointers are stored with multiplex + current_out["obj_ptr"] = multiplex_state.mux(obj_ptr) + if self.use_memory_selection: + current_out["object_score_logits"] = object_score_logits + iou_score = current_out["multistep_pred_ious"][-1].max(-1)[0] + current_out["iou_score"] = iou_score + current_out["eff_iou_score"] = self.cal_mem_score( + object_score_logits, iou_score + ) + # we need to return this for encoding new masks in the dynamic mode + current_out["object_score_logits"] = object_score_logits + + # Finally run the memory encoder on the predicted mask to encode + # it into a new memory feature (that can be used in future frames) + # (note that `self.num_maskmem == 0` is primarily used for reproducing SAM on + # images, in which case we'll just skip memory encoder to save compute). + if run_mem_encoder and self.num_maskmem > 0: + high_res_masks_for_mem_enc = high_res_masks + maskmem_features, maskmem_pos_enc = self._encode_new_memory( + image=image, + current_vision_feats=propagation_vision_feats, + feat_sizes=propagation_feat_sizes, + pred_masks_high_res=high_res_masks_for_mem_enc, + object_score_logits=object_score_logits, + is_mask_from_pts=(point_inputs is not None), + conditioning_objects=current_out["conditioning_objects"], + multiplex_state=multiplex_state, + ) + current_out["maskmem_features"] = maskmem_features + current_out["maskmem_pos_enc"] = maskmem_pos_enc + + if self.save_image_features: + current_out["image_features"] = propagation_vision_feats[-1] + current_out["image_pos_enc"] = propagation_vision_pos_embeds[-1] + + # this is to avoid recomputing some of these features for add_new_masks_to_existing_state + aux_output = {} + if need_aux_output: + if interactive_pix_feat is None: + interactive_pix_feat = self._get_interactive_pix_mem( + interactive_vision_feats, interactive_feat_sizes + ) + aux_output["interactive_pix_feat"] = interactive_pix_feat + aux_output["interactive_high_res_features"] = interactive_high_res_features + aux_output["propagation_vision_feats"] = propagation_vision_feats + aux_output["propagation_feat_sizes"] = propagation_feat_sizes + + return current_out, aux_output + + def _trim_output_and_memory( + self, + frame_idx: int, + output_dict: dict[str, dict[int, StageOutput]], + current_out: StageOutput, + memory_encoder_was_used: bool, + ) -> StageOutput: + # Optionally, offload the outputs to CPU memory during evaluation to avoid + # GPU OOM on very long videos or very large resolution or too many objects + if self.offload_output_to_cpu_for_eval and not self.training: + # Here we only keep those keys needed for evaluation to get a compact output + trimmed_out: StageOutput = { + "conditioning_objects": current_out["conditioning_objects"], + "pred_masks": current_out["pred_masks"].cpu(), + "pred_masks_high_res": current_out["pred_masks_high_res"].cpu(), + # other items for evaluation (these are small tensors so we keep them on GPU) + "object_score_logits": current_out["object_score_logits"], + "multistep_point_inputs": current_out["multistep_point_inputs"], + } + if self.use_obj_ptrs_in_encoder: + trimmed_out["obj_ptr"] = current_out["obj_ptr"] + if memory_encoder_was_used and self.num_maskmem > 0: + trimmed_out["maskmem_features"] = current_out["maskmem_features"].cpu() + trimmed_out["maskmem_pos_enc"] = [ + x.cpu() for x in current_out["maskmem_pos_enc"] + ] + if self.save_image_features: + trimmed_out["image_features"] = current_out["image_features"].cpu() + trimmed_out["image_pos_enc"] = current_out["image_pos_enc"].cpu() + current_out = trimmed_out + + # Optionally, trim the output of past non-conditioning frame (r * num_maskmem frames + # before the current frame) during evaluation. This is intended to save GPU or CPU + # memory for semi-supervised VOS eval, where only the first frame receives prompts. + def _trim_past_out( + past_out: StageOutput, current_out: StageOutput + ) -> Optional[StageOutput]: + if past_out is None: + return None + trimmed_past_out: StageOutput = { + "conditioning_objects": past_out["conditioning_objects"], + "pred_masks": past_out["pred_masks"], + "object_score_logits": past_out["object_score_logits"], + # Why would this be current_out? + # "multistep_point_inputs": current_out["multistep_point_inputs"], + "multistep_point_inputs": past_out["multistep_point_inputs"], + } + if self.use_obj_ptrs_in_encoder: + trimmed_past_out["obj_ptr"] = past_out["obj_ptr"] + return trimmed_past_out + + if self.trim_past_non_cond_mem_for_eval and not self.training: + r = self.memory_temporal_stride_for_eval + past_frame_idx = frame_idx - r * self.num_maskmem + past_out = output_dict["non_cond_frame_outputs"].get(past_frame_idx, None) + + if past_out is not None: + if ( + self.use_memory_selection + and past_out.get("eff_iou_score", 0) < self.mf_threshold + ) or not self.use_memory_selection: + output_dict["non_cond_frame_outputs"][past_frame_idx] = ( + _trim_past_out(past_out, current_out) + ) + + if ( + self.use_memory_selection and not self.offload_output_to_cpu_for_eval + ): # design for memory selection, trim too old frames to save memory + far_old_frame_idx = frame_idx - 20 * self.max_obj_ptrs_in_encoder + past_out = output_dict["non_cond_frame_outputs"].get( + far_old_frame_idx, None + ) + if past_out is not None: + output_dict["non_cond_frame_outputs"][far_old_frame_idx] = ( + _trim_past_out(past_out, current_out) + ) + + return current_out + + def track_step( + self, + *, + frame_idx, + is_init_cond_frame, + backbone_features_interactive, + backbone_features_propagation, + image, + point_inputs, + mask_inputs, + gt_masks, + frames_to_add_correction_pt, + output_dict, + num_frames, + track_in_reverse=False, # tracking in reverse time order (for demo usage) + # Whether to run the memory encoder on the predicted masks. Sometimes we might want + # to skip the memory encoder with `run_mem_encoder=False`. For example, + # in demo we might call `track_step` multiple times for each user click, + # and only encode the memory when the user finalizes their clicks. And in ablation + # settings like SAM training on static images, we don't need the memory encoder. + run_mem_encoder=True, + # The previously predicted SAM mask logits (which can be fed together with new clicks in demo). + prev_sam_mask_logits=None, + multiplex_state: MultiplexState, + # The list of object idx that point_inputs correspond to; only this set of objects will + # be interacted with in the correction stage + objects_to_interact: Optional[list[int]] = None, + ) -> StageOutput: + current_out, _ = self._track_step_aux( + frame_idx=frame_idx, + is_init_cond_frame=is_init_cond_frame, + backbone_features_interactive=backbone_features_interactive, + backbone_features_propagation=backbone_features_propagation, + image=image, + point_inputs=point_inputs, + mask_inputs=mask_inputs, + gt_masks=gt_masks, + frames_to_add_correction_pt=frames_to_add_correction_pt, + output_dict=output_dict, + num_frames=num_frames, + track_in_reverse=track_in_reverse, + run_mem_encoder=run_mem_encoder, + prev_sam_mask_logits=prev_sam_mask_logits, + multiplex_state=multiplex_state, + objects_to_interact=objects_to_interact, + need_aux_output=False, + ) + current_out = self._trim_output_and_memory( + frame_idx, output_dict, current_out, memory_encoder_was_used=run_mem_encoder + ) + + return current_out + + def back_convert(self, targets): + """To be compatible with SetCriterionAPI losses (mask loss only).""" + batched_targets = {} + batched_targets["num_boxes"] = targets.num_boxes + batched_targets["masks"] = targets.segments + batched_targets["is_valid_mask"] = targets.is_valid_segment + return batched_targets + + def _use_multimask(self, is_init_cond_frame, point_inputs): + """Whether to use multimask output in the SAM head.""" + num_pts = 0 if point_inputs is None else point_inputs["point_labels"].size(1) + multimask_output = ( + self.multimask_output_in_sam + and (is_init_cond_frame or self.multimask_output_for_tracking) + and (self.multimask_min_pt_num <= num_pts <= self.multimask_max_pt_num) + and self.num_multimask_outputs > 0 + ) + return multimask_output + + def _apply_non_overlapping_constraints(self, pred_masks): + """ + Apply non-overlapping constraints to the object scores in pred_masks. Here we + keep only the highest scoring object at each spatial location in pred_masks. + """ + batch_size = pred_masks.size(0) + if batch_size == 1: + return pred_masks + + device = pred_masks.device + # "max_obj_inds": object index of the object with the highest score at each location + max_obj_inds = torch.argmax(pred_masks, dim=0, keepdim=True) + # "batch_obj_inds": object index of each object slice (along dim 0) in `pred_masks` + batch_obj_inds = torch.arange(batch_size, device=device)[:, None, None, None] + keep = max_obj_inds == batch_obj_inds + # suppress overlapping regions' scores below -10.0 so that the foreground regions + # don't overlap (here sigmoid(-10.0)=4.5398e-05) + pred_masks = torch.where(keep, pred_masks, torch.clamp(pred_masks, max=-10.0)) + return pred_masks + + def _compile_all_components(self): + """Compile all model components for faster inference.""" + # a larger cache size to hold varying number of shapes for torch.compile + # see https://github.com/pytorch/pytorch/blob/v2.5.1/torch/_dynamo/config.py#L42-L49 + torch._dynamo.config.cache_size_limit = 64 + torch._dynamo.config.accumulated_cache_size_limit = 2048 + + logging.info("Compiling all components. First time may be very slow.") + + self.maskmem_backbone.forward = torch.compile( + self.maskmem_backbone.forward, + mode="max-autotune", + fullgraph=True, + dynamic=False, + ) + self.transformer.encoder.forward = torch.compile( + self.transformer.encoder.forward, + mode="max-autotune", + fullgraph=True, + dynamic=True, # Num. of memories varies + ) + # We disable compilation of sam_prompt_encoder as it sometimes gives a large accuracy regression, + # especially when sam_mask_prompt (previous mask logits) is not None + # self.sam_prompt_encoder.forward = torch.compile( + # self.sam_prompt_encoder.forward, + # mode="max-autotune", + # fullgraph=True, + # dynamic=False, # Accuracy regression on True + # ) + self.sam_mask_decoder.forward = torch.compile( + self.sam_mask_decoder.forward, + mode="max-autotune", + fullgraph=True, + dynamic=False, # Accuracy regression on True + ) + + def _maybe_clone(self, x): + """Clone a tensor if and only if `self.compile_all_components` is True.""" + return x.clone() if self.compile_all_components else x + + def get_propagation_dense_pe(self) -> torch.Tensor: + """ + Returns the positional encoding used to encode point prompts, + applied to a dense set of points the shape of the image encoding. + + Returns: + torch.Tensor: Positional encoding with shape + 1x(embed_dim)x(embedding_h)x(embedding_w) + """ + return self.image_pe_layer( + (self.sam_image_embedding_size, self.sam_image_embedding_size) + ).unsqueeze(0) + + def cal_mem_score(self, object_score_logits, iou_score): + object_score_norm = torch.where( + object_score_logits > 0, + object_score_logits.sigmoid() * 2 - 1, # rescale to [0, 1] + torch.zeros_like(object_score_logits), + ) + score_per_frame = (object_score_norm * iou_score).mean() + return score_per_frame + + def frame_filter(self, output_dict, track_in_reverse, frame_idx, num_frames, r): + if (frame_idx == 0 and not track_in_reverse) or ( + frame_idx == num_frames - 1 and track_in_reverse + ): + return [] + + max_num = min( + num_frames, self.max_obj_ptrs_in_encoder + ) # maximum number of pointer memory frames to consider + + if not track_in_reverse: + start = frame_idx - 1 + end = 0 + step = -r + must_include = frame_idx - 1 + else: + start = frame_idx + 1 + end = num_frames + step = r + must_include = frame_idx + 1 + + valid_indices = [] + for i in range(start, end, step): + if ( + i not in output_dict["non_cond_frame_outputs"] + or "eff_iou_score" not in output_dict["non_cond_frame_outputs"][i] + ): + continue + + score_per_frame = output_dict["non_cond_frame_outputs"][i]["eff_iou_score"] + + if score_per_frame > self.mf_threshold: # threshold + valid_indices.insert(0, i) + + if len(valid_indices) >= max_num - 1: + break + + if must_include not in valid_indices: + valid_indices.append(must_include) + + return valid_indices + + +def concat_points(old_point_inputs, new_points, new_labels): + """Add new points and labels to previous point inputs (add at the end).""" + if old_point_inputs is None: + points, labels = new_points, new_labels + else: + points = torch.cat([old_point_inputs["point_coords"], new_points], dim=1) + labels = torch.cat([old_point_inputs["point_labels"], new_labels], dim=1) + + return {"point_coords": points, "point_labels": labels} + + +def _append( + d1: StageOutput, d2: SAMOutput, k1: str, k2: str, dim: int = 0, strict: bool = True +): + if strict: + assert k1 in d1, f"{k1} not found" + else: + if k1 not in d1: + return + + d1[k1] = torch.cat([d1[k1], d2[k2]], dim=dim) + + +def _merge( + d1: StageOutput, + d2: SAMOutput, + k1: str, + k2: str, + d2_idx: list[int], + strict: bool = True, +): + if strict: + assert k1 in d1, f"{k1} not found" + else: + if k1 not in d1: + return + d1[k1][d2_idx] = d2[k2].to(dtype=d1[k1].dtype) + + +class VideoTrackingDynamicMultiplex(VideoTrackingMultiplex): + def __init__( + self, + enable_dynamic_training: bool = True, # Allows the number of objects to increase across frames during training + rand_num_transition_points: bool = True, # Randomizes the number of transition points + max_num_transition_points: int = 3, # Maximum number of transition points + add_all_transition_frames_as_cond: bool = True, + max_trans_frames_in_attn: int = 4, + is_dynamic_model: bool = True, # Overrides the default + is_dynamic_vos_evaluation: bool = False, # For datasets like YouTubeVOS which have new objects + **kwargs, + ): + super().__init__(is_dynamic_model=is_dynamic_model, **kwargs) + + self.enable_dynamic_training = enable_dynamic_training + self.rand_num_transition_points = rand_num_transition_points + self.max_num_transition_points = max_num_transition_points + + self.add_all_transition_frames_as_cond = add_all_transition_frames_as_cond + self.max_trans_frames_in_attn = max_trans_frames_in_attn + self.is_dynamic_vos_evaluation = is_dynamic_vos_evaluation + + def prepare_prompt_inputs(self, backbone_out, input, start_frame_idx=0): + """ + Prepare input mask, point or box prompts. Optionally, we allow tracking from + a custom `start_frame_idx` to the end of the video (for evaluation purposes). + """ + + """ + This function, in addition to the prompt preparation done in the parent class, preprocesses the + masks and pre-computes visibility/validity attributes necessary for training with dynamic bucketing. + + **Data** + We use a modified dataset class and a modified collate_fn such that: + 1. The mask for an object is loaded if it is visible (area>0) on any of the loaded frames + 2. A "visible_objects_per_frame" attribute is computed, which contains the set of objects with area>0 on each frame + + Here, we use [] to denote a set of objects; i.e., object A and B are represented as [A, B]. + Consider the masks given by the dataloader in an arbitrary yet deterministic order. + That is, [2, 3] can appear on the first frame, and [1, 2, 3, 17] can appear on the second frame. + + This is incompatible with the object addition implementation, since we assume new objects are appended, not inserted. + Thus, we compute object_appearance_order which sorts the object idx using the frame at which they appear + (conditional frames always appear first). For objects that appear on the same frame, we shuffle them as augmentation. + We also reorder the ground-truth masks used for supervision. + + **Causal supervision** + Since not all objects appear on the first frame, we should not supervise on the objects that the model has no knowledge of yet. + Thus, we keep track of the set of objects that have been introduced, and the frame at which that happens. + We compute valid_idx_per_frame (and correspondingly trim the ground-truth) to enforce reasonable supervisions. + + **Transition points** + Transition points are non-initial-conditioning frames that introduce new objects. We uniformly sample some frames + to be candidates for transition points, and use them if they actually introduce new objects compared to the last seen + conditional frame/transition point. + Transitions do not always happen when an object first becomes visible, because our (initial) sampling is agnostic to visibility. + This is intended, as new objects do not always get detected immediately in the dense tracking setting. + """ + + # First, prepare the prompt inputs following the parent class + backbone_out = super()._prepare_prompt_inputs_meta( + backbone_out, input, start_frame_idx=start_frame_idx + ) + + num_frames = backbone_out["num_frames"] + gt_masks_per_frame = backbone_out["gt_masks_per_frame"] + + if self.training or self.is_dynamic_vos_evaluation: + visible_objects_per_frame: dict[int, set[int]] = ( + input.visible_objects_per_frame + ) + else: + visible_objects_per_frame: dict[int, set[int]] = { + stage_id: set(range(gt_masks_per_frame[stage_id].shape[0])) + for stage_id in range(num_frames) + } + + # If we have more than one conditioning frame, + # all visible objects on any of the conditioning frames become valid for all frames + init_cond_frames: list[int] = backbone_out["init_cond_frames"] + init_cond_frames = sorted(init_cond_frames) + frames_not_in_init_cond: list[int] = backbone_out["frames_not_in_init_cond"] + + # Rare case: the data guard might fail and we could have an empty first frame. + # In this case, we track an empty object. + if len(visible_objects_per_frame[start_frame_idx]) == 0: + if self.training: + logging.warning("Empty first frame, tracking an empty object") + visible_objects_per_frame[start_frame_idx] = {0} + # set the GT mask for this object to be all zeros + for stage_id in range(num_frames): + gt_masks_per_frame[stage_id][0] = torch.zeros_like( + gt_masks_per_frame[stage_id][0] + ) + else: + # During evaluation, this should only happen for YouTubeVOS. + # We will skip the frames before the first conditional frame. + assert ( + self.is_dynamic_vos_evaluation + ), f"{visible_objects_per_frame=} invalid" + assert len(init_cond_frames) == 1 + for stage_id in range(start_frame_idx, num_frames): + if len(visible_objects_per_frame[stage_id]) > 0: + init_cond_frames = [stage_id] + break + for i in range( + init_cond_frames[0] + 1 + ): # also remove init_cond_frames[0] + if i in frames_not_in_init_cond: + frames_not_in_init_cond.remove(i) + + backbone_out["init_cond_frames"] = init_cond_frames + + # The object idx in valid_idx_per_frame should be in sequential order. + # We will first reshuffle the objects using object_appearance_order, + # and then index via valid_idx_per_frame. + valid_idx_per_frame: dict[int, list[int]] = {} + # Importantly, we cannot simply use valid_idx_per_frame[stage_id-1] because it might be a conditional frame. + valid_idx_prior_to_each_transition: dict[int, list[int]] = {} + new_idx_per_transition: dict[int, list[int]] = {} + + if self.training and self.enable_dynamic_training: + # Select the number of transition points + if self.rand_num_transition_points: + # Randomly select 1 to `max_num_transition_points` transition points + num_transition_points = self.rng.integers( + 1, self.max_num_transition_points, endpoint=True + ) + else: + num_transition_points = self.max_num_transition_points + + available_transition_points = frames_not_in_init_cond + num_transition_points = min( + num_transition_points, len(available_transition_points) + ) + # num_transition_points can differ between GPUs so we use rng2 + transition_points = self.rng2.choice( + available_transition_points, num_transition_points, replace=False + ).tolist() + transition_points = sorted(transition_points) + + # Filter for the transition points that do introduce new objects + filtered_transition_points = [] + objects_seen = set() + for stage_id in init_cond_frames: + objects_seen.update(visible_objects_per_frame[stage_id]) + + for stage_id in range(start_frame_idx, num_frames): + if stage_id in transition_points: + new_objects_seen = ( + visible_objects_per_frame[stage_id] - objects_seen + ) + if len(new_objects_seen) > 0: + filtered_transition_points.append(stage_id) + objects_seen.update(new_objects_seen) + new_idx_per_transition[stage_id] = list(new_objects_seen) + transition_points = filtered_transition_points + + # Create appearance-based object ordering with randomization + init_objects = set() + for stage_id in init_cond_frames: + init_objects.update(visible_objects_per_frame[stage_id]) + init_objects = list(init_objects) + self.rng2.shuffle(init_objects) + + object_appearance_order = init_objects.copy() + valid_idx_per_frame[start_frame_idx] = list(range(len(init_objects))) + for stage_id in range(start_frame_idx + 1, num_frames): + if stage_id in transition_points: + # When objects appear at a transition point, we add them to the end of the list + stage_objects = new_idx_per_transition[stage_id].copy() + self.rng2.shuffle(stage_objects) + valid_idx_prior_to_each_transition[stage_id] = list( + range(len(object_appearance_order)) + ) + new_idx_per_transition[stage_id] = list( + range( + len(object_appearance_order), + len(object_appearance_order) + len(stage_objects), + ) + ) + object_appearance_order.extend(stage_objects) + + # Update the valid objects at this frame + if stage_id in init_cond_frames: + # Note: on any non-first init cond frame, the number of valid objects + # might be fewer than the previous frame because we always process the init cond frames first. + # For example, if [1, 2, 4] are visible on the two init cond frames (e.g., frame 0 and frame 5), + # and object 3 appears on frame 4 (as a transition point), object 3 would not be considered valid on frame 5. + # This should not break any processing steps or affect correctness (since invalid objects are marked as floating). + valid_idx_per_frame[stage_id] = valid_idx_per_frame[ + start_frame_idx + ].copy() + elif stage_id in frames_not_in_init_cond: + valid_idx_per_frame[stage_id] = list( + range(len(object_appearance_order)) + ) + else: + raise ValueError( + f"Unexpected {stage_id=}? {init_cond_frames=} {frames_not_in_init_cond=} {transition_points=}" + ) + elif self.is_dynamic_vos_evaluation and not self.training: + # In dynamic VOS evaluation, we find the transition points manually. + # Each object should appear on exactly one frame. + # NOTE: The new release of YouTubeVOS apparently did not enforce this. + # We are enforcing it here. + + # Find first appearance of each object + object_appearance_order: list[int] = [] + object_appear_at_stage: dict[int, int] = {} + transition_points: list[int] = [] + stage_to_new_objects: dict[int, list[int]] = defaultdict(list) + for stage_id in range(start_frame_idx, num_frames): + visible_objects = sorted(list(visible_objects_per_frame[stage_id])) + for obj_id in visible_objects: + if obj_id in object_appear_at_stage: + continue # skip seen objects + + object_appear_at_stage[obj_id] = stage_id + object_appearance_order.append(obj_id) + stage_to_new_objects[stage_id].append(obj_id) + if stage_id not in init_cond_frames: + transition_points.append(stage_id) + + # Track cumulative object count + objects_seen_so_far = [] + for stage_id in range(start_frame_idx, num_frames): + if stage_id in transition_points: + # New objects appear at this frame + new_objects = stage_to_new_objects[stage_id] + num_objects_before = len(objects_seen_so_far) + + # Record which objects were valid before this transition + valid_idx_prior_to_each_transition[stage_id] = list( + range(num_objects_before) + ) + # Record the indices of new objects + new_idx_per_transition[stage_id] = list( + range(num_objects_before, num_objects_before + len(new_objects)) + ) + + objects_seen_so_far.extend(new_objects) + + # Set valid objects for this frame + if stage_id in init_cond_frames: + # For init cond frames, only the initial objects are valid + valid_idx_per_frame[stage_id] = list( + range(len(stage_to_new_objects[stage_id])) + ) + objects_seen_so_far.extend(stage_to_new_objects[stage_id]) + else: + # For other frames, all objects seen so far are valid + valid_idx_per_frame[stage_id] = list( + range(len(objects_seen_so_far)) + ) + + else: + # Use no transition points when dynamic training is disabled + transition_points = [] + visible_objects_on_first_frame = sorted( + list(visible_objects_per_frame[start_frame_idx]) + ) + # Since visible_objects_on_first_frame might not be consecutive + object_orderings = list(range(len(visible_objects_on_first_frame))) + # Use the original order for evaluation + object_appearance_order = visible_objects_on_first_frame.copy() + for stage_id in range(start_frame_idx, num_frames): + valid_idx_per_frame[stage_id] = object_orderings.copy() + + # Apply the appearance-based mapping to ground-truth masks + for stage_id in range(start_frame_idx, num_frames): + gt_masks_per_frame[stage_id] = gt_masks_per_frame[stage_id][ + object_appearance_order + ][valid_idx_per_frame[stage_id]] + + # We also want to apply this change in-place to the input, such that loss can be computed correctly. + # For targets.segments, we need to delay the object introduction by 1 frame. + # At transition points, use current frame's masks but only for objects that existed in the previous frame. + # This allows us to compute the loss on the existing objects and not on the newly added objects. + for stage_id, targets in enumerate(input.find_targets): + if stage_id in transition_points: + # At transition points, use current frame's masks but only keep objects from the previous frame + prev_objects = valid_idx_prior_to_each_transition[stage_id] + # Only keep masks for objects that existed in the previous frame + targets.segments = gt_masks_per_frame[stage_id][prev_objects].squeeze(1) + else: + targets.segments = gt_masks_per_frame[stage_id].squeeze(1) + # Ensure that we are averaging the loss correctly. + # Although this is called num_boxes, it actually stores an array of ones with length=number of objects in the VOS setting. + targets.num_boxes = targets.num_boxes[: targets.segments.shape[0]] + + backbone_out["valid_idx_per_frame"] = valid_idx_per_frame + backbone_out["new_idx_per_transition"] = new_idx_per_transition + backbone_out["valid_objects_prior_to_each_transition"] = ( + valid_idx_prior_to_each_transition + ) + backbone_out["transition_points"] = set(transition_points) + backbone_out["gt_masks_per_frame"] = gt_masks_per_frame + backbone_out["object_appearance_order"] = object_appearance_order + + backbone_out = self._prepare_conditional_frames(backbone_out) + + return backbone_out + + def add_new_masks_to_existing_state( + self, + *, + interactive_pix_feat: torch.Tensor, + interactive_high_res_features: list[torch.Tensor], + propagation_vision_feats: Optional[ + list[torch.Tensor] + ], # needed when add_mask_to_memory=True + propagation_feat_sizes: Optional[ + list[tuple[int, int]] + ], # needed when add_mask_to_memory=True + new_masks: torch.Tensor, + obj_idxs_in_mask: list[ + int + ], # len(obj_idxs_in_mask) == new_masks.shape[0]; object idx internal to this state + obj_ids_in_mask: Optional[ + list[int] + ], # len(obj_ids_in_mask) == new_masks.shape[0]; global object ids + prev_output: StageOutput, # this state will be modified in-place + multiplex_state: MultiplexState, + add_mask_to_memory: bool = True, + are_masks_from_pts: bool = False, + allow_new_buckets: bool = False, + prefer_new_buckets: bool = False, + ) -> None: + """ + Add new objects to an existing output/multiplex state. + + This function encodes the input masks as new masks and merges them with the existing state. + The new object entries are always appended to the existing objects. + + This is because, in the dense tracking scenario, we should always propagate (existing state) + to the current frame first before introducing the new objects. + """ + assert self.use_mask_input_as_output_without_sam + assert new_masks.shape[0] == len(obj_idxs_in_mask) + + num_new_objects = new_masks.shape[0] + + if obj_ids_in_mask is not None: + assert len(obj_ids_in_mask) == num_new_objects + + if self.use_obj_ptrs_in_encoder: + # demux the existing pointers before we change the multiplex state + existing_pointers = multiplex_state.demux(prev_output["obj_ptr"]) + + # Step 1: Inform the multiplex state that we are adding new objects + new_object_idx = multiplex_state.find_next_batch_of_available_indices( + num_objects=num_new_objects, + allow_new_buckets=allow_new_buckets, + prefer_new_buckets=prefer_new_buckets, + ) + multiplex_state.add_objects( + object_indices=new_object_idx, + object_ids=obj_ids_in_mask, + allow_new_buckets=allow_new_buckets, + prefer_new_buckets=prefer_new_buckets, + ) + + # Step 2: Encode the incoming masks + mask_output = self._use_mask_as_output( + backbone_features=interactive_pix_feat, + high_res_features=interactive_high_res_features, + mask_inputs=new_masks, + multiplex_state=multiplex_state, + objects_in_mask=new_object_idx, + ) + + # Step 3: Merge the existing state with new encoded features + # Handle resolution mismatch between propagation (e.g., 1008) and interactive (e.g., 288) features + # Determine target resolution from interactive features (newly generated masks) + interactive_resolution = mask_output["high_res_masks"].shape[-1] + + # Check if prev_output needs resolution adjustment + if ( + "pred_masks_high_res" in prev_output + and prev_output["pred_masks_high_res"] is not None + ): + existing_resolution = prev_output["pred_masks_high_res"].shape[-1] + + if existing_resolution != interactive_resolution: + # Resize existing outputs to match interactive resolution + # This happens when frame was bootstrapped with propagation features (1008) + # but we're now adding interactive masks (288) + prev_output["pred_masks_high_res"] = F.interpolate( + prev_output["pred_masks_high_res"], + size=(interactive_resolution, interactive_resolution), + mode="bilinear", + align_corners=False, + ) + + # Resize low_res_masks to match prev_output resolution + h, w = prev_output["pred_masks"].shape[-2:] + mask_output["low_res_masks"] = F.interpolate( + mask_output["low_res_masks"], + size=(h, w), + align_corners=False, + mode="bilinear", + antialias=True, # use antialias for downsampling + ) + + _append(prev_output, mask_output, "pred_masks", "low_res_masks") + _append( + prev_output, + mask_output, + "pred_masks_high_res", + "high_res_masks", + strict=False, + ) + _append(prev_output, mask_output, "object_score_logits", "object_score_logits") + if self.use_memory_selection: + mask_output["ious"] = mask_output["ious"].squeeze(-1) + _append(prev_output, mask_output, "iou_score", "ious") + + # Merge the input masks + if "input_masks" in prev_output: + prev_output["input_masks"] = torch.cat( + [prev_output["input_masks"], new_masks], dim=0 + ) + + if self.use_obj_ptrs_in_encoder: + # Merge the object pointers. Note that the pointers in SAMOutput are in the data space, + # while those in StageOutput are in the mux space. + new_pointers = mask_output["obj_ptr"].to(existing_pointers.dtype) + combined_pointers = torch.cat([existing_pointers, new_pointers], dim=0) + prev_output["obj_ptr"] = multiplex_state.mux(combined_pointers) + + # Step 4: Update the set of conditioning objects at this frame. + prev_output["conditioning_objects"].update(new_object_idx) + + # Step 5: Re-encode the spatial memory if needed + if add_mask_to_memory: + assert ( + prev_output["pred_masks_high_res"].shape[0] + == multiplex_state.total_valid_entries + ) + # Add the new masks to the memory + maskmem_features, maskmem_pos_enc = self._encode_new_memory( + image=None, + current_vision_feats=propagation_vision_feats, + feat_sizes=propagation_feat_sizes, + pred_masks_high_res=prev_output["pred_masks_high_res"], + object_score_logits=prev_output["object_score_logits"], + conditioning_objects=prev_output["conditioning_objects"], + is_mask_from_pts=are_masks_from_pts, + multiplex_state=multiplex_state, + ) + prev_output["maskmem_features"] = maskmem_features + prev_output["maskmem_pos_enc"] = maskmem_pos_enc + if self.save_image_features: + # They should already be in the state; no modification is needed + assert "image_features" in prev_output + assert "image_pos_enc" in prev_output + + def recondition_masks_in_existing_state( + self, + *, + interactive_pix_feat: torch.Tensor, + interactive_high_res_features: list[torch.Tensor], + propagation_vision_feats: Optional[ + list[torch.Tensor] + ], # needed when add_mask_to_memory=True + propagation_feat_sizes: Optional[ + list[tuple[int, int]] + ], # needed when add_mask_to_memory=True + new_masks: torch.Tensor, + obj_idxs_in_mask: list[ + int + ], # len(obj_idxs_in_mask) == new_masks.shape[0]; object idx internal to this state + obj_ids_in_mask: Optional[ + list[int] + ], # len(obj_ids_in_mask) == new_masks.shape[0]; global object ids + prev_output: StageOutput, # this state will be modified in-place + multiplex_state: MultiplexState, + add_mask_to_memory: bool = True, + ) -> None: + """ + Recondition existing objects in an existing output/multiplex state. + + This function encodes the input masks and merges them with the existing state. + """ + assert self.use_mask_input_as_output_without_sam + assert new_masks.shape[0] == len(obj_idxs_in_mask) + + num_new_objects = new_masks.shape[0] + + if obj_ids_in_mask is not None: + assert len(obj_ids_in_mask) == num_new_objects + + if self.use_obj_ptrs_in_encoder: + # demux the existing pointers before we change the multiplex state + existing_pointers = multiplex_state.demux(prev_output["obj_ptr"]) + + # Step 1: Encode the incoming masks + mask_output = self._use_mask_as_output( + backbone_features=interactive_pix_feat, + high_res_features=interactive_high_res_features, + mask_inputs=new_masks, + multiplex_state=multiplex_state, + objects_in_mask=obj_idxs_in_mask, + ) + + # Step 2: Merge the existing state with new encoded features + # TODO: Remove this and fix the resolution mismatch + h, w = prev_output["pred_masks"].shape[-2:] + mask_output["low_res_masks"] = F.interpolate( + mask_output["low_res_masks"], + size=(h, w), + align_corners=False, + mode="bilinear", + antialias=True, # use antialias for downsampling + ) + + _merge( + prev_output, mask_output, "pred_masks", "low_res_masks", obj_idxs_in_mask + ) + _merge( + prev_output, + mask_output, + "pred_masks_high_res", + "high_res_masks", + obj_idxs_in_mask, + strict=False, + ) + _merge( + prev_output, + mask_output, + "object_score_logits", + "object_score_logits", + obj_idxs_in_mask, + ) + if self.use_memory_selection: + mask_output["ious"] = mask_output["ious"].squeeze(-1) + _merge( + prev_output, + mask_output, + "iou_score", + "ious", + obj_idxs_in_mask, + ) + + # Merge the input masks + if "input_masks" in prev_output: + prev_output["input_masks"][obj_idxs_in_mask] = new_masks + + if self.use_obj_ptrs_in_encoder: + # Merge the object pointers. Note that the pointers in SAMOutput are in the data space, + # while those in StageOutput are in the mux space. + new_pointers = mask_output["obj_ptr"].to(existing_pointers.dtype) + existing_pointers[obj_idxs_in_mask] = new_pointers + prev_output["obj_ptr"] = multiplex_state.mux(existing_pointers) + + # Step 3: Update the set of conditioning objects at this frame + prev_output["conditioning_objects"].update(obj_idxs_in_mask) + + # Step 4: Re-encode the spatial memory if needed + if add_mask_to_memory: + assert ( + prev_output["pred_masks_high_res"].shape[0] + == multiplex_state.total_valid_entries + ) + # Add the new masks to the memory + maskmem_features, maskmem_pos_enc = self._encode_new_memory( + image=None, + current_vision_feats=propagation_vision_feats, + feat_sizes=propagation_feat_sizes, + pred_masks_high_res=prev_output["pred_masks_high_res"], + object_score_logits=prev_output["object_score_logits"], + conditioning_objects=prev_output["conditioning_objects"], + is_mask_from_pts=False, + multiplex_state=multiplex_state, + ) + prev_output["maskmem_features"] = maskmem_features + prev_output["maskmem_pos_enc"] = maskmem_pos_enc + if self.save_image_features: + # They should already be in the state; no modification is needed + assert "image_features" in prev_output + assert "image_pos_enc" in prev_output + + def track_step( + self, + *, + frame_idx, + is_init_cond_frame, + backbone_features_interactive, + backbone_features_propagation, + image, + point_inputs, + mask_inputs, + gt_masks, + frames_to_add_correction_pt, + output_dict, + num_frames, + track_in_reverse=False, # tracking in reverse time order (for demo usage) + # Whether to run the memory encoder on the predicted masks. Sometimes we might want + # to skip the memory encoder with `run_mem_encoder=False`. For example, + # in demo we might call `track_step` multiple times for each user click, + # and only encode the memory when the user finalizes their clicks. And in ablation + # settings like SAM training on static images, we don't need the memory encoder. + run_mem_encoder=True, + # The previously predicted SAM mask logits (which can be fed together with new clicks in demo). + prev_sam_mask_logits=None, + multiplex_state: MultiplexState, + # The list of object IDs that point_inputs correspond to; only this set of objects will + # be interacted with in the correction stage + objects_to_interact: Optional[list[int]] = None, + # The following parameters are specific to the dynamic multiplexing model + new_object_masks: Optional[torch.Tensor] = None, + new_object_idxs: Optional[list[int]] = None, + new_object_ids: Optional[list[int]] = None, + are_new_masks_from_pts: bool = False, + ) -> StageOutput: + # First, run track_step_aux. + # This includes propagation, interaction, and correction. + current_out, aux_out = self._track_step_aux( + frame_idx=frame_idx, + is_init_cond_frame=is_init_cond_frame, + backbone_features_interactive=backbone_features_interactive, + backbone_features_propagation=backbone_features_propagation, + image=image, + point_inputs=point_inputs, + mask_inputs=mask_inputs, + gt_masks=gt_masks, + frames_to_add_correction_pt=frames_to_add_correction_pt, + output_dict=output_dict, + num_frames=num_frames, + track_in_reverse=track_in_reverse, + run_mem_encoder=(run_mem_encoder and new_object_masks is None), + prev_sam_mask_logits=prev_sam_mask_logits, + multiplex_state=multiplex_state, + objects_to_interact=objects_to_interact, + need_aux_output=(new_object_masks is not None), + ) + + # If new masks are provided, merge them into the existing state + if new_object_masks is not None: + assert new_object_idxs is not None + self.add_new_masks_to_existing_state( + interactive_pix_feat=aux_out["interactive_pix_feat"], + interactive_high_res_features=aux_out["interactive_high_res_features"], + propagation_vision_feats=aux_out["propagation_vision_feats"], + propagation_feat_sizes=aux_out["propagation_feat_sizes"], + new_masks=new_object_masks, + obj_idxs_in_mask=new_object_idxs, + obj_ids_in_mask=new_object_ids, + prev_output=current_out, + multiplex_state=multiplex_state, + add_mask_to_memory=run_mem_encoder, + are_masks_from_pts=are_new_masks_from_pts, + ) + + # lastly, trim the output + current_out = self._trim_output_and_memory( + frame_idx=frame_idx, + output_dict=output_dict, + current_out=current_out, + memory_encoder_was_used=run_mem_encoder, + ) + + return current_out + + def forward_tracking( + self, + backbone_out, + input, + return_dict=False, + objects_to_interact: Optional[list[int]] = None, + ): + """Forward video tracking on each frame (and sample correction clicks).""" + img_feats_already_computed = ( + "interactive" in backbone_out or "sam2_backbone_out" in backbone_out + ) + if img_feats_already_computed: + # Prepare the backbone features + # - vision_feats and vision_pos_embeds are in (HW)BC format + # - vision_masks are in B(HW) format, dtype=bool (False is valid, True is padding) + backbone_features = self._prepare_backbone_features(backbone_out) + + # Starting the stage loop + num_frames = backbone_out["num_frames"] + init_cond_frames = backbone_out["init_cond_frames"] + frames_to_add_correction_pt = backbone_out["frames_to_add_correction_pt"] + # First process all the initial conditioning frames to encode them as memory, + # And then condition on them to track the remaining frames + processing_order = init_cond_frames + backbone_out["frames_not_in_init_cond"] + + new_idx_per_transition = backbone_out["new_idx_per_transition"] + valid_objects_prior_to_each_transition = backbone_out[ + "valid_objects_prior_to_each_transition" + ] + transition_points = backbone_out["transition_points"] + + cond_frame_outputs: dict[int, StageOutput] = {} + non_cond_frame_outputs: dict[int, StageOutput] = {} + output_dict = { + "cond_frame_outputs": cond_frame_outputs, + "non_cond_frame_outputs": non_cond_frame_outputs, + } + multiplex_state = self.multiplex_controller.get_state( + backbone_out["gt_masks_per_frame"][processing_order[0]].shape[0], + device=backbone_out["gt_masks_per_frame"][processing_order[0]].device, + dtype=torch.float, + random=self.training, + ) + + for stage_id in processing_order: + # Get the image features for the current frame + img_ids = input.find_inputs[stage_id].img_ids + # The image ids are for the entire batch + assert all( + [img_id == img_ids[0] for img_id in img_ids] + ) # should be all the same + # force this to have a batch size of 1 + img_ids = torch.tensor( + [img_ids[0]], device=img_ids.device, dtype=img_ids.dtype + ) + + if img_feats_already_computed: + # Retrieve image features according to img_ids (if they are already computed). + current_image = input.img_batch.tensors[img_ids] + current_backbone_features = {} + for neck_k, neck_out in backbone_features.items(): + current_backbone_features[neck_k] = { + "vision_feats": [ + x[:, img_ids] for x in neck_out["vision_feats"] + ], + "vision_masks": [ + x[img_ids] if x is not None else None + for x in neck_out["vision_masks"] + ], + "vision_pos_embeds": [ + x[:, img_ids] for x in neck_out["vision_pos_embeds"] + ], + "feat_sizes": neck_out["feat_sizes"], + } + else: + # Otherwise, compute the image features on the fly for the given img_ids + # (this might be used for evaluation on long videos to avoid backbone OOM). + need_interactive_out = ( + (stage_id in frames_to_add_correction_pt) + or (stage_id in init_cond_frames) + or (stage_id in transition_points) + ) + (current_image, current_backbone_features) = ( + self._prepare_backbone_features_per_frame( + input.img_batch, + img_ids, + need_interactive_out=need_interactive_out, + need_propagation_out=True, + ) + ) + + gt_masks = backbone_out["gt_masks_per_frame"].get(stage_id, None) + if stage_id in transition_points: + assert gt_masks is not None + + # Figure out new object masks / idxs + new_object_idxs = new_idx_per_transition[stage_id] + # Get the new object masks, ensure correct ordering + assert sorted(new_object_idxs) == new_object_idxs + assert ( + new_object_idxs[0] + == len(valid_objects_prior_to_each_transition[stage_id]) + ), f"{new_object_idxs=}; {gt_masks.shape=}; {valid_objects_prior_to_each_transition[stage_id]=}" + assert new_object_idxs[-1] == ( + len(gt_masks) - 1 + ), f"{new_object_idxs=}; {gt_masks.shape=}" + new_object_masks = gt_masks[new_object_idxs] + + # Remove the new objects from the gt masks + gt_masks = gt_masks[: new_object_idxs[0]] + else: + new_object_masks = None + new_object_idxs = None + + # Get output masks based on this frame's prompts and previous memory + current_out = self.track_step( + frame_idx=stage_id, + is_init_cond_frame=stage_id in init_cond_frames, + backbone_features_interactive=current_backbone_features.get( + "interactive" + ), + backbone_features_propagation=current_backbone_features.get( + "sam2_backbone_out" + ), + image=current_image, + point_inputs=backbone_out["point_inputs_per_frame"].get(stage_id, None), + mask_inputs=backbone_out["mask_inputs_per_frame"].get(stage_id, None), + gt_masks=gt_masks, + frames_to_add_correction_pt=frames_to_add_correction_pt, + output_dict=output_dict, + num_frames=num_frames, + multiplex_state=multiplex_state, + objects_to_interact=objects_to_interact, + new_object_masks=new_object_masks, + new_object_idxs=new_object_idxs, + ) + # Append the output, depending on whether it's a conditioning frame + add_output_as_cond_frame = ( + stage_id in init_cond_frames + or ( + self.add_all_frames_to_correct_as_cond + and stage_id in frames_to_add_correction_pt + ) + or ( + self.add_all_transition_frames_as_cond + and stage_id in transition_points + ) + ) + + if add_output_as_cond_frame: + output_dict["cond_frame_outputs"][stage_id] = current_out + else: + output_dict["non_cond_frame_outputs"][stage_id] = current_out + + output_dict["multiplex_state"] = multiplex_state + + if return_dict: + return output_dict + # turn `output_dict` into a list for loss function + all_frame_outputs = {} + all_frame_outputs.update(output_dict["cond_frame_outputs"]) + all_frame_outputs.update(output_dict["non_cond_frame_outputs"]) + if self.is_dynamic_vos_evaluation: + all_frame_outputs = [all_frame_outputs.get(t) for t in range(num_frames)] + else: + all_frame_outputs = [all_frame_outputs[t] for t in range(num_frames)] + # Make DDP happy with activation checkpointing by removing unused keys + all_frame_outputs = [ + {k: v for k, v in d.items() if k != "obj_ptr"} if d is not None else None + for d in all_frame_outputs + ] + + if self.is_dynamic_vos_evaluation: + object_appearance_order = backbone_out["object_appearance_order"] + num_objects = len(input.find_metadatas[0].coco_image_id) + + # since we have remapped the object appearance order, we would need to map it back here + inverse_object_appearance_order = [None for _ in object_appearance_order] + for idx, obj_id in enumerate(object_appearance_order): + inverse_object_appearance_order[obj_id] = idx + assert all(i is not None for i in inverse_object_appearance_order) + + # this is for a rare case where the dataloader thinks that there is an object + # (is in input.find_metadatas[0].coco_image_id) + # but it is not visible anywhere in the frames + # I suspect this is due to mask resizing (the object is so small that it got lost) + # but I am not 100% sure; haven't investigated yet. + # This only happens if we evaluate on the new (fully annotated) YouTubeVOS set. + if len(inverse_object_appearance_order) < num_objects: + inverse_object_appearance_order.extend( + list(range(len(inverse_object_appearance_order), num_objects)) + ) + + # we need to pad the outputs with zeros (for the frames before the object appears) + last_mask = all_frame_outputs[-1]["pred_masks"] + + shape = last_mask.shape[1:] + dtype = last_mask.dtype + device = last_mask.device + for stage_i, frame_out in enumerate(all_frame_outputs): + if frame_out is None: + all_frame_outputs[stage_i] = { + "pred_masks": torch.zeros( + (num_objects, *shape), device=device, dtype=dtype + ) + } + continue + + pred_mask = frame_out["pred_masks"] + if pred_mask.shape[0] < num_objects: + shape = pred_mask.shape[ + 1: + ] # might have a different shape, e.g., input mask + frame_out["pred_masks"] = torch.cat( + [ + pred_mask, + torch.zeros( + (num_objects - pred_mask.shape[0], *shape), + device=device, + dtype=dtype, + ), + ], + dim=0, + )[inverse_object_appearance_order] + + return all_frame_outputs diff --git a/sam3/model/video_tracking_multiplex_demo.py b/sam3/model/video_tracking_multiplex_demo.py new file mode 100644 index 0000000..c572577 --- /dev/null +++ b/sam3/model/video_tracking_multiplex_demo.py @@ -0,0 +1,3476 @@ +import logging +from collections import OrderedDict +from copy import deepcopy +from typing import Iterable, Optional + +import numpy as np +import torch +from sam3.model.data_misc import NestedTensor +from sam3.model.io_utils import load_video_frames +from sam3.model.multiplex_utils import MultiplexState +from sam3.model.sam3_tracker_utils import fill_holes_in_mask_scores +from sam3.model.video_tracking_multiplex import ( + concat_points, + NO_OBJ_SCORE, + VideoTrackingDynamicMultiplex, +) +from tqdm import tqdm + + +class VideoTrackingMultiplexDemo(VideoTrackingDynamicMultiplex): + """ + The demo class that extends the `VideoTrackingDynamicMultiplex` to handle user interactions + and manage inference states, with support for multi-object tracking. + + Interactions are not yet implemented. + """ + + def __init__( + self, + # whether to clear non-conditioning memory of the surrounding frames (which may contain outdated information) after adding correction clicks; + # note that this would only apply to *single-object tracking* unless `clear_non_cond_mem_for_multi_obj` is also set to True) + clear_non_cond_mem_around_input=False, + # whether to also clear non-conditioning memory of the surrounding frames (only effective when `clear_non_cond_mem_around_input` is True). + clear_non_cond_mem_for_multi_obj=False, + # if fill_hole_area > 0, we fill small holes in the final masks up to this area (after resizing them to the original video resolution) + fill_hole_area=0, + # if always_start_from_first_ann_frame is True, we always start tracking from the frame where we receive the first annotation (clicks or mask) + # and ignore the `start_frame_idx` passed to `propagate_in_video` + always_start_from_first_ann_frame=False, + # the maximum number of points to be used in the prompt encoder, which reduce the domain gap between training (that only has 8 points) + # - if it's set to a positive integer, we only take the `max_point_num_in_prompt_enc//2` points and + # the last `(max_point_num_in_prompt_enc - max_point_num_in_prompt_enc//2)` points in the prompt encoder + # - if it's set to 0 or negative, this option is turned off and we use all points in the prompt encoder + max_point_num_in_prompt_enc=16, + non_overlap_masks_for_output=True, + **kwargs, + ): + super().__init__(**kwargs) + + self.clear_non_cond_mem_around_input = clear_non_cond_mem_around_input + self.clear_non_cond_mem_for_multi_obj = clear_non_cond_mem_for_multi_obj + self.fill_hole_area = fill_hole_area + self.always_start_from_first_ann_frame = always_start_from_first_ann_frame + self.max_point_num_in_prompt_enc = max_point_num_in_prompt_enc + self.non_overlap_masks_for_output = non_overlap_masks_for_output + + @torch.inference_mode() + def init_state( + self, + video_path, + offload_video_to_cpu, + offload_state_to_cpu, + async_loading_frames=False, + use_torchcodec=False, + use_cv2=False, + ): + """Initialize a inference state.""" + # Make sure that sigmoid is used on mask logits (should be True for all our recent models). + # Since we rely on large negative values as scores for missing objects, the raw logits + # cannot be consumed directly and must be converted into 0~1 range via sigmoid first. + if not self.apply_sigmoid_to_mask_logits_for_mem_enc: + raise NotImplementedError( + "Multi-object tracking requires sigmoid in memory encoder for non-overlapping constraints." + ) + + images, video_height, video_width = load_video_frames( + video_path=video_path, + image_size=self.image_size, + offload_video_to_cpu=offload_video_to_cpu, + async_loading_frames=async_loading_frames, + use_torchcodec=use_torchcodec, + use_cv2=use_cv2, + ) + inference_state = {} + inference_state["images"] = images + inference_state["num_frames"] = len(images) + # whether to offload the video frames to CPU memory + # turning on this option saves the GPU memory with only a very small overhead + inference_state["offload_video_to_cpu"] = offload_video_to_cpu + # whether to offload the inference state to CPU memory + # turning on this option saves the GPU memory at the cost of a lower tracking fps + # (e.g. in a test case of 768x768 model, fps dropped from 27 to 24 when tracking one object + # and from 24 to 21 when tracking two objects) + inference_state["offload_state_to_cpu"] = offload_state_to_cpu + # the original video height and width, used for resizing final output scores + inference_state["video_height"] = video_height + inference_state["video_width"] = video_width + inference_state["device"] = torch.device("cuda") + if offload_state_to_cpu: + inference_state["storage_device"] = torch.device("cpu") + else: + inference_state["storage_device"] = torch.device("cuda") + # inputs on each frame + inference_state["point_inputs_per_obj"] = {} + inference_state["mask_inputs_per_obj"] = {} + # visual features on a small number of recently visited frames for quick interactions + inference_state["cached_features"] = {} + # values that don't change across frames (so we only need to hold one copy of them) + inference_state["constants"] = {} + # mapping between client-side object id and model-side object index + inference_state["obj_id_to_idx"] = OrderedDict() + inference_state["obj_idx_to_id"] = OrderedDict() + inference_state["obj_ids"] = [] + # A storage to hold the model's tracking results and states on each frame + inference_state["output_dict"] = { + "cond_frame_outputs": {}, # dict containing {frame_idx: } + "non_cond_frame_outputs": {}, # dict containing {frame_idx: } + } + # The index of the frame that received the first annotation + inference_state["first_ann_frame_idx"] = None + # Slice (view) of each object tracking results, sharing the same memory with "output_dict" + inference_state["output_dict_per_obj"] = {} + # A temporary storage to hold new outputs when user interact with a frame + # to add clicks or mask (it's merged into "output_dict" before propagation starts) + inference_state["temp_output_dict_per_obj"] = {} + # Frames that already holds consolidated outputs from click or mask inputs + # (we directly use their consolidated outputs during tracking) + inference_state["consolidated_frame_inds"] = { + "cond_frame_outputs": set(), # set containing frame indices + "non_cond_frame_outputs": set(), # set containing frame indices + } + # metadata for each tracking frame (e.g. which direction it's tracked) + inference_state["tracking_has_started"] = False + inference_state["frames_already_tracked"] = {} + inference_state["multiplex_state"] = None + # Track which frames have been refined by user interaction (per object) + # This is used to distinguish first refinement (fresh) vs subsequent refinements (incremental) + inference_state["user_refined_frames_per_obj"] = {} + # # Warm up the whole model and cache the image feature on frame 0 + # # by making a dummy click on the first frame (and then cleaning it up) + # self.add_new_points( + # inference_state=inference_state, + # frame_idx=0, + # obj_id=1, + # points=torch.tensor([[0.5, 0.5]], dtype=torch.float32), + # labels=torch.tensor([1], dtype=torch.int32), + # clear_old_points=True, + # rel_coordinates=True, + # ) + # self.clear_all_points_in_video(inference_state) + return inference_state + + def _obj_id_to_idx(self, inference_state, obj_id, error_if_new=False): + """Map client-side object id to model-side object index.""" + obj_idx = inference_state["obj_id_to_idx"].get(obj_id, None) + if obj_idx is not None: + return obj_idx + + if ( + self.is_dynamic_model or not inference_state["tracking_has_started"] + ) and not error_if_new: + # get the next object slot + obj_idx = len(inference_state["obj_id_to_idx"]) + inference_state["obj_id_to_idx"][obj_id] = obj_idx + inference_state["obj_idx_to_id"][obj_idx] = obj_id + inference_state["obj_ids"] = list(inference_state["obj_id_to_idx"]) + # set up input and output structures for this object + inference_state["point_inputs_per_obj"][obj_idx] = {} + inference_state["mask_inputs_per_obj"][obj_idx] = {} + inference_state["output_dict_per_obj"][obj_idx] = { + "cond_frame_outputs": {}, # dict containing {frame_idx: } + "non_cond_frame_outputs": {}, # dict containing {frame_idx: } + } + inference_state["temp_output_dict_per_obj"][obj_idx] = { + "cond_frame_outputs": {}, # dict containing {frame_idx: } + "non_cond_frame_outputs": {}, # dict containing {frame_idx: } + } + return obj_idx + else: + raise RuntimeError( + f"Cannot add new object id {obj_id}. " + f"All existing object ids: {inference_state['obj_ids']}." + ) + + def _obj_idx_to_id(self, inference_state, obj_idx): + """Map model-side object index to client-side object id.""" + return inference_state["obj_idx_to_id"][obj_idx] + + def _get_obj_num(self, inference_state): + """Get the total number of unique object ids received so far in this session.""" + # return len(inference_state["obj_idx_to_id"]) + return inference_state["multiplex_state"].total_valid_entries + + @torch.inference_mode() + def _extract_object_for_interaction(self, inference_state, obj_id, frame_idx): + """ + Extract a single object from multiplex state for singleton interaction. + Adapted from sam3_multiplex_tracking._extract_object_to_singleton_state() + + Returns: + singleton_state: New inference state containing only this object + obj_idx_in_source: Original object index before removal (for merging back) + """ + source_state = inference_state + obj_idx_in_source = source_state["obj_id_to_idx"][obj_id] + + # Step 1: Extract all object data BEFORE removing it + multiplex_state = source_state.get("multiplex_state") + + # Extract consolidated outputs (slice NOW before remove_object modifies tensors) + singleton_consolidated_outputs = { + "cond_frame_outputs": {}, + "non_cond_frame_outputs": {}, + } + + if "output_dict" in source_state: + for storage_key in ["cond_frame_outputs", "non_cond_frame_outputs"]: + source_outputs = source_state["output_dict"].get(storage_key, {}) + + for f_idx, source_frame_out in source_outputs.items(): + # Check if this frame has valid data for this object + has_valid_data = ( + source_frame_out["pred_masks"].shape[0] >= obj_idx_in_source + 1 + ) + + if has_valid_data: + # Create singleton frame output by slicing + singleton_frame_out = { + "pred_masks": source_frame_out["pred_masks"][ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone(), + "object_score_logits": source_frame_out[ + "object_score_logits" + ][obj_idx_in_source : obj_idx_in_source + 1].clone(), + # image_features and image_pos_enc remain shared (not in multiplex space) + "image_features": source_frame_out.get("image_features"), + "image_pos_enc": source_frame_out.get("image_pos_enc"), + "local_obj_id_to_idx": {obj_id: 0}, + } + + # Handle maskmem_features by converting from multiplex space to data space + maskmem_features = source_frame_out.get("maskmem_features") + if maskmem_features is not None: + if multiplex_state is not None: + expected_buckets = multiplex_state.num_buckets + expected_multiplex = multiplex_state.multiplex_count + if ( + maskmem_features.dim() >= 2 + and maskmem_features.shape[0] == expected_buckets + and maskmem_features.shape[1] == expected_multiplex + ): + try: + demuxed_features = multiplex_state.demux( + maskmem_features + ) + except AssertionError as exc: + logging.warning( + "[EXTRACT] demux failed for maskmem_features shape %s: %s", + tuple(maskmem_features.shape), + exc, + ) + demuxed_features = None + if demuxed_features is not None: + maskmem_features = demuxed_features[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + else: + maskmem_features = maskmem_features[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + elif maskmem_features.shape[0] == 0: + # No entries for this object yet; treat as missing without warning + maskmem_features = None + elif maskmem_features.shape[0] >= obj_idx_in_source + 1: + # Already in data space; slice directly + maskmem_features = maskmem_features[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + else: + logging.warning( + "[EXTRACT] maskmem_features shape %s incompatible with multiplex state; dropping tensor", + tuple(maskmem_features.shape), + ) + maskmem_features = None + else: + maskmem_features = maskmem_features[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + singleton_frame_out["maskmem_features"] = maskmem_features + + # Handle maskmem_pos_enc similarly, level by level + maskmem_pos_enc = source_frame_out.get("maskmem_pos_enc") + if maskmem_pos_enc is not None: + remapped_pos_enc = [] + for level_enc in maskmem_pos_enc: + if level_enc is None: + remapped_pos_enc.append(None) + continue + if multiplex_state is not None: + expected_buckets = multiplex_state.num_buckets + expected_multiplex = multiplex_state.multiplex_count + if ( + level_enc.dim() >= 2 + and level_enc.shape[0] == expected_buckets + and level_enc.shape[1] == expected_multiplex + ): + try: + demuxed_level = multiplex_state.demux( + level_enc + ) + except AssertionError as exc: + logging.warning( + "[EXTRACT] demux failed for maskmem_pos_enc level shape %s: %s", + tuple(level_enc.shape), + exc, + ) + demuxed_level = None + if demuxed_level is not None: + remapped_pos_enc.append( + demuxed_level[ + obj_idx_in_source : obj_idx_in_source + + 1 + ].clone() + ) + elif ( + level_enc.shape[0] >= obj_idx_in_source + 1 + ): + remapped_pos_enc.append( + level_enc[ + obj_idx_in_source : obj_idx_in_source + + 1 + ].clone() + ) + else: + logging.warning( + "[EXTRACT] maskmem_pos_enc level shape %s incompatible with multiplex state; dropping level", + tuple(level_enc.shape), + ) + remapped_pos_enc.append(None) + elif level_enc.shape[0] >= obj_idx_in_source + 1: + remapped_pos_enc.append( + level_enc[ + obj_idx_in_source : obj_idx_in_source + + 1 + ].clone() + ) + else: + logging.warning( + "[EXTRACT] maskmem_pos_enc level shape %s incompatible with multiplex state; dropping level", + tuple(level_enc.shape), + ) + remapped_pos_enc.append(None) + else: + remapped_pos_enc.append( + level_enc[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + ) + maskmem_pos_enc = remapped_pos_enc + singleton_frame_out["maskmem_pos_enc"] = maskmem_pos_enc + + # Handle obj_ptr (must demux from multiplex space first) + if ( + "obj_ptr" in source_frame_out + and self.use_obj_ptrs_in_encoder + ): + source_obj_ptr = source_frame_out["obj_ptr"] + if multiplex_state is not None: + # Demux: multiplex space → data space + obj_ptr_data_space = multiplex_state.demux( + source_obj_ptr + ) + # Slice for this object + singleton_frame_out["obj_ptr"] = obj_ptr_data_space[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + else: + singleton_frame_out["obj_ptr"] = source_obj_ptr[ + obj_idx_in_source : obj_idx_in_source + 1 + ].clone() + + # Convert conditioning_objects + if "conditioning_objects" in source_frame_out: + if ( + obj_idx_in_source + in source_frame_out["conditioning_objects"] + ): + singleton_frame_out["conditioning_objects"] = {0} + else: + singleton_frame_out["conditioning_objects"] = set() + + singleton_consolidated_outputs[storage_key][f_idx] = ( + singleton_frame_out + ) + + # Extract point and mask inputs + extracted_point_inputs = {} + extracted_mask_inputs = {} + + if ( + "point_inputs_per_obj" in source_state + and obj_idx_in_source in source_state["point_inputs_per_obj"] + ): + extracted_point_inputs = source_state["point_inputs_per_obj"][ + obj_idx_in_source + ].copy() + + if ( + "mask_inputs_per_obj" in source_state + and obj_idx_in_source in source_state["mask_inputs_per_obj"] + ): + extracted_mask_inputs = source_state["mask_inputs_per_obj"][ + obj_idx_in_source + ].copy() + + # Extract per-object outputs + extracted_obj_cond_outputs = {} + extracted_obj_non_cond_outputs = {} + extracted_temp_cond_outputs = {} + extracted_temp_non_cond_outputs = {} + + if ( + "output_dict_per_obj" in source_state + and obj_idx_in_source in source_state["output_dict_per_obj"] + ): + obj_output_dict = source_state["output_dict_per_obj"][obj_idx_in_source] + extracted_obj_cond_outputs = obj_output_dict.get( + "cond_frame_outputs", {} + ).copy() + extracted_obj_non_cond_outputs = obj_output_dict.get( + "non_cond_frame_outputs", {} + ).copy() + + if ( + "temp_output_dict_per_obj" in source_state + and obj_idx_in_source in source_state["temp_output_dict_per_obj"] + ): + temp_obj_output_dict = source_state["temp_output_dict_per_obj"][ + obj_idx_in_source + ] + extracted_temp_cond_outputs = temp_obj_output_dict.get( + "cond_frame_outputs", {} + ).copy() + extracted_temp_non_cond_outputs = temp_obj_output_dict.get( + "non_cond_frame_outputs", {} + ).copy() + + # Step 2: Remove the object from source state + remaining_obj_ids, _ = self.remove_object( + source_state, + obj_id, + strict=False, + need_output=False, + clear_user_refined_map=False, + ) + + # If multiplex state became empty, reset it so downstream code can reinitialize + updated_multiplex_state = source_state.get("multiplex_state") + if updated_multiplex_state is not None: + if ( + getattr(updated_multiplex_state, "assignments", None) is None + or updated_multiplex_state.total_valid_entries == 0 + ): + source_state["multiplex_state"] = None + + # Step 3: Create new singleton inference state + singleton_state = self.init_state( + cached_features=source_state["cached_features"], + video_height=source_state["video_height"], + video_width=source_state["video_width"], + num_frames=source_state["num_frames"], + ) + + # Step 4: Set up singleton state structure + singleton_state["obj_id_to_idx"] = {obj_id: 0} + singleton_state["obj_idx_to_id"] = {0: obj_id} + singleton_state["obj_ids"] = [obj_id] + singleton_state["point_inputs_per_obj"] = {0: extracted_point_inputs} + singleton_state["mask_inputs_per_obj"] = {0: extracted_mask_inputs} + singleton_state["output_dict_per_obj"] = { + 0: { + "cond_frame_outputs": extracted_obj_cond_outputs, + "non_cond_frame_outputs": extracted_obj_non_cond_outputs, + } + } + singleton_state["temp_output_dict_per_obj"] = { + 0: { + "cond_frame_outputs": extracted_temp_cond_outputs, + "non_cond_frame_outputs": extracted_temp_non_cond_outputs, + } + } + singleton_state["frames_already_tracked"] = source_state[ + "frames_already_tracked" + ].copy() + + # Step 5: Create new singleton multiplex state (even for 1 object, needed for obj_ptr) + new_multiplex_state = self.multiplex_controller.get_state( + num_valid_entries=1, + device=source_state["device"], + dtype=torch.float32, + random=False, + object_ids=[obj_id], + ) + singleton_state["multiplex_state"] = new_multiplex_state + + # Step 6: Remux extracted tensors into the singleton multiplex space + for storage_key in ["cond_frame_outputs", "non_cond_frame_outputs"]: + for f_idx, frame_out in singleton_consolidated_outputs[storage_key].items(): + # mask memory features + if frame_out.get("maskmem_features") is not None: + # Keep mask memory features in data space (num_objects, C, H, W) + frame_out["maskmem_features"] = frame_out[ + "maskmem_features" + ].clone() + + if frame_out.get("maskmem_pos_enc") is not None: + remapped_levels = [] + for level_enc in frame_out["maskmem_pos_enc"]: + if level_enc is None: + remapped_levels.append(None) + continue + remapped_levels.append(level_enc.clone()) + frame_out["maskmem_pos_enc"] = remapped_levels + + # object pointers + if "obj_ptr" in frame_out and self.use_obj_ptrs_in_encoder: + # Mux: data space [1, D] → singleton multiplex space [1, 1, D] + frame_out["obj_ptr"] = new_multiplex_state.mux(frame_out["obj_ptr"]) + + singleton_state["output_dict"] = singleton_consolidated_outputs + + return singleton_state, obj_idx_in_source + + @torch.inference_mode() + def _merge_singleton_interaction_result( + self, + inference_state, + singleton_state, + obj_id, + original_obj_idx, + ): + """ + Merge singleton interaction result back into multiplex state. + + SIMPLIFIED APPROACH: Add object back at the END (new index), not at original position. + This avoids complex index shifting and works with multiplex controller's add_objects() API. + + Args: + inference_state: The main multiplex inference state + singleton_state: The singleton state with interaction results + obj_id: The object ID + original_obj_idx: The original index before extraction (unused - we add at end instead) + """ + # Determine new index (add at end) + new_obj_idx = len(inference_state["obj_ids"]) + + # Step 1: Add object mappings at new index + inference_state["obj_ids"].append(obj_id) + inference_state["obj_id_to_idx"][obj_id] = new_obj_idx + + # Create entry in output_dict_per_obj and temp_output_dict_per_obj for new index + # These are DICTIONARIES indexed by obj_idx, not lists! + inference_state["output_dict_per_obj"][new_obj_idx] = { + "cond_frame_outputs": {}, + "non_cond_frame_outputs": {}, + } + inference_state["temp_output_dict_per_obj"][new_obj_idx] = { + "cond_frame_outputs": {}, + "non_cond_frame_outputs": {}, + } + + inference_state["obj_idx_to_id"][new_obj_idx] = obj_id + + # Step 2: Add object to multiplex state buckets using proper API + multiplex_state = inference_state.get("multiplex_state") + + assignments = ( + getattr(multiplex_state, "assignments", None) + if multiplex_state is not None + else None + ) + total_valid_entries = ( + getattr(multiplex_state, "total_valid_entries", 0) + if multiplex_state is not None and assignments is not None + else 0 + ) + need_state_reinit = ( + multiplex_state is None or assignments is None or total_valid_entries == 0 + ) + + if not need_state_reinit and getattr(multiplex_state, "object_ids", None): + if obj_id in multiplex_state.object_ids: + old_idx = multiplex_state.object_ids.index(obj_id) + multiplex_state.remove_objects(object_indices=[old_idx], strict=False) + assignments = getattr(multiplex_state, "assignments", None) + total_valid_entries = ( + getattr(multiplex_state, "total_valid_entries", 0) + if assignments is not None + else 0 + ) + need_state_reinit = assignments is None or total_valid_entries == 0 + + if need_state_reinit: + inference_state["multiplex_state"] = self.multiplex_controller.get_state( + num_valid_entries=len(inference_state["obj_ids"]), + device=inference_state["device"], + dtype=torch.float32, + random=False, + object_ids=list(inference_state["obj_ids"]), + ) + multiplex_state = inference_state["multiplex_state"] + else: + # Allow new buckets since we're adding at a new index (the old bucket slot may have been removed) + multiplex_state.add_objects( + object_indices=[new_obj_idx], + object_ids=[obj_id], + allow_new_buckets=True, # May need new bucket if old slot was compacted + ) + + # Step 3: Restore point and mask inputs at new index + singleton_obj_idx = 0 # Object is always at index 0 in singleton state + if ( + "point_inputs_per_obj" in singleton_state + and singleton_obj_idx in singleton_state["point_inputs_per_obj"] + ): + if "point_inputs_per_obj" not in inference_state: + inference_state["point_inputs_per_obj"] = {} + inference_state["point_inputs_per_obj"][new_obj_idx] = singleton_state[ + "point_inputs_per_obj" + ][singleton_obj_idx].copy() + + if ( + "mask_inputs_per_obj" in singleton_state + and singleton_obj_idx in singleton_state["mask_inputs_per_obj"] + ): + if "mask_inputs_per_obj" not in inference_state: + inference_state["mask_inputs_per_obj"] = {} + inference_state["mask_inputs_per_obj"][new_obj_idx] = singleton_state[ + "mask_inputs_per_obj" + ][singleton_obj_idx].copy() + + # Step 4: Restore per-object outputs at new index + if ( + "output_dict_per_obj" in singleton_state + and singleton_obj_idx in singleton_state["output_dict_per_obj"] + ): + if "output_dict_per_obj" not in inference_state: + inference_state["output_dict_per_obj"] = {} + inference_state["output_dict_per_obj"][new_obj_idx] = singleton_state[ + "output_dict_per_obj" + ][singleton_obj_idx].copy() + + if ( + "temp_output_dict_per_obj" in singleton_state + and singleton_obj_idx in singleton_state["temp_output_dict_per_obj"] + ): + if "temp_output_dict_per_obj" not in inference_state: + inference_state["temp_output_dict_per_obj"] = {} + inference_state["temp_output_dict_per_obj"][new_obj_idx] = singleton_state[ + "temp_output_dict_per_obj" + ][singleton_obj_idx].copy() + + # Step 5: Merge consolidated outputs back into multiplex (append at new_obj_idx) + # Preserve each frame's original storage key from the singleton state so that + # conditioning frames remain in cond_frame_outputs after the merge. + if "output_dict" in singleton_state: + singleton_multiplex_state = singleton_state.get("multiplex_state") + for singleton_storage_key in [ + "cond_frame_outputs", + "non_cond_frame_outputs", + ]: + singleton_outputs = singleton_state["output_dict"].get( + singleton_storage_key, {} + ) + + # Skip if singleton doesn't have any frames in this storage_key + if not singleton_outputs: + continue + + for frame_idx, singleton_frame_out in singleton_outputs.items(): + # Get or create frame output in main state at the EXPECTED storage_key + if "output_dict" not in inference_state: + inference_state["output_dict"] = { + "cond_frame_outputs": {}, + "non_cond_frame_outputs": {}, + } + + if ( + frame_idx + not in inference_state["output_dict"][singleton_storage_key] + ): + # Frame doesn't exist - create with singleton results at new_obj_idx + num_objs = len(inference_state["obj_ids"]) + + # Ensure num_objs is at least new_obj_idx + 1 + # (in case obj_ids list is somehow inconsistent) + if num_objs <= new_obj_idx: + num_objs = new_obj_idx + 1 + + new_maskmem_features = None + new_maskmem_pos_enc = None + if ( + singleton_frame_out.get("maskmem_features") is not None + and multiplex_state is not None + ): + # Check if singleton features are in multiplexed format and demux if needed + singleton_features_muxed = singleton_frame_out[ + "maskmem_features" + ] + if singleton_features_muxed.shape[:2] == ( + singleton_multiplex_state.num_buckets, + singleton_multiplex_state.multiplex_count, + ): + # Singleton features are multiplexed, need to demux + singleton_features_data = ( + singleton_multiplex_state.demux( + singleton_features_muxed + ) + ) + else: + # Singleton features are in data space + singleton_features_data = singleton_features_muxed + + feature_shape = (num_objs,) + singleton_features_data.shape[ + 1: + ] + maskmem_features_data = torch.zeros( + feature_shape, + dtype=singleton_features_data.dtype, + device=singleton_features_data.device, + ) + maskmem_features_data[new_obj_idx : new_obj_idx + 1] = ( + singleton_features_data + ) + # Mux using destination multiplex state + new_maskmem_features = multiplex_state.mux( + maskmem_features_data + ) + + if ( + singleton_frame_out.get("maskmem_pos_enc") is not None + and multiplex_state is not None + ): + new_maskmem_pos_enc = [] + for level_enc in singleton_frame_out["maskmem_pos_enc"]: + if level_enc is None: + new_maskmem_pos_enc.append(None) + continue + # Check if singleton pos_enc is in multiplexed format and demux if needed + if level_enc.shape[:2] == ( + singleton_multiplex_state.num_buckets, + singleton_multiplex_state.multiplex_count, + ): + # Singleton pos_enc is multiplexed, need to demux + level_data = singleton_multiplex_state.demux( + level_enc + ) + else: + # Singleton pos_enc is in data space + level_data = level_enc + + level_shape = (num_objs,) + level_data.shape[1:] + level_tensor = torch.zeros( + level_shape, + dtype=level_data.dtype, + device=level_data.device, + ) + level_tensor[new_obj_idx : new_obj_idx + 1] = level_data + # Mux using destination multiplex state to store in multiplex format + new_maskmem_pos_enc.append( + multiplex_state.mux(level_tensor) + ) + + inference_state["output_dict"][singleton_storage_key][ + frame_idx + ] = { + "maskmem_features": new_maskmem_features, + "maskmem_pos_enc": new_maskmem_pos_enc, + "image_features": singleton_frame_out.get("image_features"), + "image_pos_enc": singleton_frame_out.get("image_pos_enc"), + "local_obj_id_to_idx": {obj_id: new_obj_idx}, + "conditioning_objects": ( + set([new_obj_idx]) + if singleton_obj_idx + in singleton_frame_out.get( + "conditioning_objects", set() + ) + else set() + ), + "pred_masks": torch.zeros( + ( + num_objs, + 1, + singleton_frame_out["pred_masks"].shape[2], + singleton_frame_out["pred_masks"].shape[3], + ), + dtype=singleton_frame_out["pred_masks"].dtype, + device=singleton_frame_out["pred_masks"].device, + ), + "object_score_logits": torch.full( + (num_objs, 1), + NO_OBJ_SCORE, + dtype=singleton_frame_out["object_score_logits"].dtype, + device=singleton_frame_out[ + "object_score_logits" + ].device, + ), + } + # Set singleton results at new_obj_idx + inference_state["output_dict"][singleton_storage_key][ + frame_idx + ]["pred_masks"][ + new_obj_idx : new_obj_idx + 1 + ] = singleton_frame_out["pred_masks"] + inference_state["output_dict"][singleton_storage_key][ + frame_idx + ]["object_score_logits"][ + new_obj_idx : new_obj_idx + 1 + ] = singleton_frame_out["object_score_logits"] + + # Also copy pred_masks_video_res if it exists in singleton output + if "pred_masks_video_res" in singleton_frame_out: + inference_state["output_dict"][singleton_storage_key][ + frame_idx + ]["pred_masks_video_res"] = torch.zeros( + ( + num_objs, + 1, + singleton_frame_out["pred_masks_video_res"].shape[ + 2 + ], + singleton_frame_out["pred_masks_video_res"].shape[ + 3 + ], + ), + dtype=singleton_frame_out["pred_masks_video_res"].dtype, + device=singleton_frame_out[ + "pred_masks_video_res" + ].device, + ) + inference_state["output_dict"][singleton_storage_key][ + frame_idx + ]["pred_masks_video_res"][ + new_obj_idx : new_obj_idx + 1 + ] = singleton_frame_out["pred_masks_video_res"] + + # Handle obj_ptr if present + if ( + "obj_ptr" in singleton_frame_out + and self.use_obj_ptrs_in_encoder + ): + singleton_obj_ptr_data = singleton_multiplex_state.demux( + singleton_frame_out["obj_ptr"] + ) + obj_ptr_data = torch.zeros( + (num_objs, singleton_obj_ptr_data.shape[1]), + dtype=singleton_obj_ptr_data.dtype, + device=singleton_obj_ptr_data.device, + ) + obj_ptr_data[new_obj_idx : new_obj_idx + 1] = ( + singleton_obj_ptr_data + ) + inference_state["output_dict"][singleton_storage_key][ + frame_idx + ]["obj_ptr"] = multiplex_state.mux(obj_ptr_data) + else: + # Frame exists - expand tensors and add singleton results + main_frame_out = inference_state["output_dict"][ + singleton_storage_key + ][frame_idx] + + num_objs_total = len(inference_state["obj_ids"]) + + if ( + singleton_frame_out.get("maskmem_features") is not None + and multiplex_state is not None + ): + # Check if singleton features are in multiplexed format and demux if needed + singleton_features_muxed = singleton_frame_out[ + "maskmem_features" + ] + if singleton_features_muxed.shape[:2] == ( + singleton_multiplex_state.num_buckets, + singleton_multiplex_state.multiplex_count, + ): + # Singleton features are multiplexed, need to demux + singleton_features_data = ( + singleton_multiplex_state.demux( + singleton_features_muxed + ) + ) + else: + # Singleton features are in data space + singleton_features_data = singleton_features_muxed + + existing_features_muxed = main_frame_out.get( + "maskmem_features" + ) + if existing_features_muxed is not None: + # Check if features are in multiplex format before demuxing + if existing_features_muxed.shape[:2] == ( + multiplex_state.num_buckets, + multiplex_state.multiplex_count, + ): + # Features are in multiplex format, demux them + existing_features_data = multiplex_state.demux( + existing_features_muxed + ) + else: + # Features are already in data space, use directly + existing_features_data = existing_features_muxed + else: + existing_features_data = None + + if existing_features_data is None: + feature_shape = ( + num_objs_total, + ) + singleton_features_data.shape[1:] + existing_features_data = torch.zeros( + feature_shape, + dtype=singleton_features_data.dtype, + device=singleton_features_data.device, + ) + elif existing_features_data.shape[0] < num_objs_total: + pad_size = ( + num_objs_total - existing_features_data.shape[0] + ) + pad = torch.zeros( + (pad_size,) + existing_features_data.shape[1:], + dtype=existing_features_data.dtype, + device=existing_features_data.device, + ) + existing_features_data = torch.cat( + [existing_features_data, pad], dim=0 + ) + + existing_features_data[new_obj_idx : new_obj_idx + 1] = ( + singleton_features_data + ) + main_frame_out["maskmem_features"] = multiplex_state.mux( + existing_features_data + ) + + if ( + singleton_frame_out.get("maskmem_pos_enc") is not None + and multiplex_state is not None + ): + existing_pos_enc_list = ( + main_frame_out.get("maskmem_pos_enc") or [] + ) + new_maskmem_pos_enc = [] + max_levels = max( + len(singleton_frame_out["maskmem_pos_enc"]), + len(existing_pos_enc_list), + ) + for level_idx in range(max_levels): + singleton_level_muxed = ( + singleton_frame_out["maskmem_pos_enc"][level_idx] + if level_idx + < len(singleton_frame_out["maskmem_pos_enc"]) + else None + ) + existing_level_muxed = ( + existing_pos_enc_list[level_idx] + if level_idx < len(existing_pos_enc_list) + else None + ) + + if singleton_level_muxed is None: + # Keep existing entry (which may also be None) + new_maskmem_pos_enc.append(existing_level_muxed) + continue + + # Check if singleton pos_enc is in multiplexed format and demux if needed + if singleton_level_muxed.shape[:2] == ( + singleton_multiplex_state.num_buckets, + singleton_multiplex_state.multiplex_count, + ): + # Singleton pos_enc is multiplexed, need to demux + singleton_level_data = ( + singleton_multiplex_state.demux( + singleton_level_muxed + ) + ) + else: + # Singleton pos_enc is in data space + singleton_level_data = singleton_level_muxed + + if existing_level_muxed is not None: + # Check if pos_enc is in multiplex format before demuxing + if existing_level_muxed.shape[:2] == ( + multiplex_state.num_buckets, + multiplex_state.multiplex_count, + ): + # Positional encoding is in multiplex format, demux it + existing_level_data = multiplex_state.demux( + existing_level_muxed + ) + else: + # Positional encoding is already in data space, use directly + existing_level_data = existing_level_muxed + else: + existing_level_data = None + + if existing_level_data is None: + level_shape = ( + num_objs_total, + ) + singleton_level_data.shape[1:] + existing_level_data = torch.zeros( + level_shape, + dtype=singleton_level_data.dtype, + device=singleton_level_data.device, + ) + elif existing_level_data.shape[0] < num_objs_total: + pad_size = ( + num_objs_total - existing_level_data.shape[0] + ) + pad = torch.zeros( + (pad_size,) + existing_level_data.shape[1:], + dtype=existing_level_data.dtype, + device=existing_level_data.device, + ) + existing_level_data = torch.cat( + [existing_level_data, pad], dim=0 + ) + + existing_level_data[new_obj_idx : new_obj_idx + 1] = ( + singleton_level_data + ) + new_maskmem_pos_enc.append( + multiplex_state.mux(existing_level_data) + ) + + main_frame_out["maskmem_pos_enc"] = new_maskmem_pos_enc + + singleton_pred_masks = singleton_frame_out[ + "pred_masks" + ] # [1, 1, H, W] + singleton_scores = singleton_frame_out[ + "object_score_logits" + ] # [1, 1] + + # Expand tensors if needed + num_existing_objs = main_frame_out["pred_masks"].shape[0] + if new_obj_idx >= num_existing_objs: + num_objs_needed = new_obj_idx + 1 + pad_size = num_objs_needed - num_existing_objs + + main_frame_out["pred_masks"] = torch.cat( + [ + main_frame_out["pred_masks"], + torch.zeros( + ( + pad_size, + 1, + singleton_pred_masks.shape[2], + singleton_pred_masks.shape[3], + ), + dtype=singleton_pred_masks.dtype, + device=singleton_pred_masks.device, + ), + ], + dim=0, + ) + + main_frame_out["object_score_logits"] = torch.cat( + [ + main_frame_out["object_score_logits"], + torch.full( + (pad_size, 1), + NO_OBJ_SCORE, + dtype=singleton_scores.dtype, + device=singleton_scores.device, + ), + ], + dim=0, + ) + + # Set singleton results at new_obj_idx + main_frame_out["pred_masks"][new_obj_idx : new_obj_idx + 1] = ( + singleton_pred_masks + ) + main_frame_out["object_score_logits"][ + new_obj_idx : new_obj_idx + 1 + ] = singleton_scores + # Initialize local_obj_id_to_idx if missing (e.g., frame + # output was created by VG propagation's track_step which + # does not populate this field). + if "local_obj_id_to_idx" not in main_frame_out: + main_frame_out["local_obj_id_to_idx"] = deepcopy( + inference_state["obj_id_to_idx"] + ) + main_frame_out["local_obj_id_to_idx"][obj_id] = new_obj_idx + + # Also expand and copy pred_masks_video_res if it exists in singleton output + if "pred_masks_video_res" in singleton_frame_out: + if "pred_masks_video_res" in main_frame_out: + # Expand existing video_res masks + if ( + main_frame_out["pred_masks_video_res"].shape[0] + < new_obj_idx + 1 + ): + pad_size = ( + new_obj_idx + + 1 + - main_frame_out["pred_masks_video_res"].shape[ + 0 + ] + ) + main_frame_out["pred_masks_video_res"] = torch.cat( + [ + main_frame_out["pred_masks_video_res"], + torch.zeros( + ( + pad_size, + 1, + singleton_frame_out[ + "pred_masks_video_res" + ].shape[2], + singleton_frame_out[ + "pred_masks_video_res" + ].shape[3], + ), + dtype=singleton_frame_out[ + "pred_masks_video_res" + ].dtype, + device=singleton_frame_out[ + "pred_masks_video_res" + ].device, + ), + ], + dim=0, + ) + else: + # Create new video_res masks tensor + num_objs = len(inference_state["obj_ids"]) + main_frame_out["pred_masks_video_res"] = torch.zeros( + ( + num_objs, + 1, + singleton_frame_out[ + "pred_masks_video_res" + ].shape[2], + singleton_frame_out[ + "pred_masks_video_res" + ].shape[3], + ), + dtype=singleton_frame_out[ + "pred_masks_video_res" + ].dtype, + device=singleton_frame_out[ + "pred_masks_video_res" + ].device, + ) + # Set singleton video_res mask + main_frame_out["pred_masks_video_res"][ + new_obj_idx : new_obj_idx + 1 + ] = singleton_frame_out["pred_masks_video_res"] + + # Handle obj_ptr + if ( + "obj_ptr" in singleton_frame_out + and self.use_obj_ptrs_in_encoder + ): + singleton_obj_ptr_data = singleton_multiplex_state.demux( + singleton_frame_out["obj_ptr"] + ) # [1, D] + + if "obj_ptr" in main_frame_out: + # The existing obj_ptr may have been created with a DIFFERENT number of buckets + # (before we called multiplex_state.add_objects() which may have created new buckets). + # We need to infer the OLD bucket count from the tensor shape to demux it correctly. + + old_obj_ptr_muxed = main_frame_out["obj_ptr"] + # Infer old bucket count: shape is [B_old, M_old, D] + old_num_buckets = old_obj_ptr_muxed.shape[1] + + # Create temporary multiplex state with old bucket count to demux + if old_num_buckets != multiplex_state.num_buckets: + # Bucket count changed - cannot safely demux old obj_ptr + # Instead, create new obj_ptr from scratch for all objects + num_objs = len(inference_state["obj_ids"]) + obj_ptr_data = torch.zeros( + (num_objs, singleton_obj_ptr_data.shape[1]), + dtype=singleton_obj_ptr_data.dtype, + device=singleton_obj_ptr_data.device, + ) + # Only set the singleton object's ptr, leave others as zeros + obj_ptr_data[new_obj_idx : new_obj_idx + 1] = ( + singleton_obj_ptr_data + ) + main_frame_out["obj_ptr"] = multiplex_state.mux( + obj_ptr_data + ) + else: + # Bucket count matches - safe to demux + main_obj_ptr_data = multiplex_state.demux( + old_obj_ptr_muxed + ) + + # Expand if needed + if main_obj_ptr_data.shape[0] < new_obj_idx + 1: + pad_size = ( + new_obj_idx + 1 - main_obj_ptr_data.shape[0] + ) + main_obj_ptr_data = torch.cat( + [ + main_obj_ptr_data, + torch.zeros( + ( + pad_size, + main_obj_ptr_data.shape[1], + ), + dtype=main_obj_ptr_data.dtype, + device=main_obj_ptr_data.device, + ), + ], + dim=0, + ) + + main_obj_ptr_data[new_obj_idx : new_obj_idx + 1] = ( + singleton_obj_ptr_data + ) + main_frame_out["obj_ptr"] = multiplex_state.mux( + main_obj_ptr_data + ) + else: + # Create new obj_ptr + num_objs = len(inference_state["obj_ids"]) + obj_ptr_data = torch.zeros( + (num_objs, singleton_obj_ptr_data.shape[1]), + dtype=singleton_obj_ptr_data.dtype, + device=singleton_obj_ptr_data.device, + ) + obj_ptr_data[new_obj_idx : new_obj_idx + 1] = ( + singleton_obj_ptr_data + ) + main_frame_out["obj_ptr"] = multiplex_state.mux( + obj_ptr_data + ) + + # Update conditioning_objects + if singleton_obj_idx in singleton_frame_out.get( + "conditioning_objects", set() + ): + main_frame_out["conditioning_objects"].add(new_obj_idx) + + @torch.inference_mode() + def add_new_points( + self, + inference_state, + frame_idx, + obj_id, + points, + labels, + clear_old_points, + rel_coordinates=True, + use_prev_mem_frame=False, + ): + """ + Add new points to create a new object in the multiplex model. + + This method converts point inputs to masks via the interactivity head and adds + the new object to the existing multiplex bucket (for dynamic models). + + Args: + inference_state: Current inference state + frame_idx: Frame index to add points + obj_id: Object ID (will be auto-created if new) + points: Point coordinates tensor + labels: Point labels tensor (1 for positive, 0 for negative) + clear_old_points: Whether to clear old points on this frame + rel_coordinates: Whether points are in relative coordinates [0, 1] + use_prev_mem_frame: Whether to use previous memory frames (for compatibility) + + Returns: + Tuple of (frame_idx, obj_ids, low_res_masks, video_res_masks) + """ + obj_idx = self._obj_id_to_idx(inference_state, obj_id) + obj_idxs = [obj_idx] + obj_ids = [obj_id] + + point_inputs_per_frame = inference_state["point_inputs_per_obj"][obj_idx] + mask_inputs_per_frame = inference_state["mask_inputs_per_obj"][obj_idx] + + if points.dim() == 2: + points = points.unsqueeze(0) + if labels.dim() == 1: + labels = labels.unsqueeze(0) + + if rel_coordinates: + points = points * self.image_size + + points = points.to(inference_state["device"]) + labels = labels.to(inference_state["device"]) + + if not clear_old_points: + old_point_inputs = point_inputs_per_frame.get(frame_idx, None) + else: + old_point_inputs = None + + point_inputs = concat_points(old_point_inputs, points, labels) + point_inputs_per_frame[frame_idx] = point_inputs + + is_init_cond_frame = frame_idx not in inference_state["frames_already_tracked"] + + if is_init_cond_frame: + reverse = False + else: + reverse = inference_state["frames_already_tracked"][frame_idx]["reverse"] + + is_cond = is_init_cond_frame or self.add_all_frames_to_correct_as_cond + storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs" + + multiplex_state = inference_state["multiplex_state"] + is_new_state = multiplex_state is None + + if is_new_state: + multiplex_state = self.multiplex_controller.get_state( + num_valid_entries=1, + device=inference_state["device"], + dtype=torch.float32, + random=False, + object_ids=obj_ids, + ) + inference_state["multiplex_state"] = multiplex_state + + # Determine interaction case: + # - New object: never seen before + # - Refine: existing mask on tracked frame + # - Gap fill: object exists but frame has no output + is_existing_object = ( + not is_new_state + and multiplex_state is not None + and obj_id in multiplex_state.object_ids + ) + + if is_existing_object: + if is_init_cond_frame: + is_new_obj = False + is_refine = False + is_gap_fill_case = True + else: + is_new_obj = False + is_refine = True + is_gap_fill_case = False + else: + is_new_obj = True + is_refine = False + is_gap_fill_case = False + + if is_new_obj: + should_add_to_existing = not is_new_state + allow_new_buckets_local = True + prefer_new_buckets_local = True + + current_out, _ = self._run_single_frame_inference( + inference_state=inference_state, + output_dict=inference_state["output_dict"], + frame_idx=frame_idx, + batch_size=1, + is_init_cond_frame=True, + point_inputs=point_inputs, + mask_inputs=None, + reverse=False, + run_mem_encoder=False, + prev_sam_mask_logits=None, + add_to_existing_state=should_add_to_existing, + new_obj_idxs=obj_idxs, + new_obj_ids=obj_ids, + allow_new_buckets=allow_new_buckets_local, + prefer_new_buckets=prefer_new_buckets_local, + objects_to_interact=None, + ) + elif is_refine: + singleton_state, original_obj_idx = self._extract_object_for_interaction( + inference_state, obj_id, frame_idx + ) + + user_refined_frames_map = inference_state.get( + "user_refined_frames_per_obj", {} + ) + user_refined_frames = user_refined_frames_map.get(obj_id) + if user_refined_frames is None: + user_refined_frames = set() + is_first_refinement = frame_idx not in user_refined_frames + + prev_sam_mask_logits_singleton = None + if not is_first_refinement: + singleton_obj_idx = 0 + singleton_output_dict = singleton_state["output_dict_per_obj"][ + singleton_obj_idx + ] + singleton_temp_output_dict = singleton_state[ + "temp_output_dict_per_obj" + ][singleton_obj_idx] + + # Check BOTH storage keys since previous refinement might be in a different key + # (e.g., first refinement creates cond_frame, but after propagation, + # second refinement on same frame would look for non_cond_frame) + prev_out = None + + storage_key_current = ( + "cond_frame_outputs" if is_cond else "non_cond_frame_outputs" + ) + prev_out = singleton_temp_output_dict[storage_key_current].get( + frame_idx + ) + + if prev_out is None: + prev_out = singleton_output_dict["cond_frame_outputs"].get( + frame_idx + ) + if prev_out is None: + prev_out = singleton_output_dict["non_cond_frame_outputs"].get( + frame_idx + ) + + if prev_out is not None and prev_out["pred_masks"] is not None: + prev_sam_mask_logits_singleton = prev_out["pred_masks"].cuda( + non_blocking=True + ) + prev_sam_mask_logits_singleton = torch.clamp( + prev_sam_mask_logits_singleton, -32.0, 32.0 + ) + + if is_first_refinement: + # ALWAYS use is_init_cond_frame=True to force interaction_only mode + # for fresh segmentation from points (not refinement of propagated mask). + singleton_is_init_cond = True + singleton_objects_to_interact = None + else: + # Second+ refinement: Incremental refinement for quality improvement + singleton_is_init_cond = False + singleton_objects_to_interact = ( + [0] if prev_sam_mask_logits_singleton is not None else None + ) + + singleton_obj_idx = 0 + singleton_obj_idxs = [singleton_obj_idx] + singleton_obj_ids = [obj_id] + + current_out, _ = self._run_single_frame_inference( + inference_state=singleton_state, + output_dict=singleton_state["output_dict"], + frame_idx=frame_idx, + batch_size=1, + is_init_cond_frame=singleton_is_init_cond, + point_inputs=point_inputs, + mask_inputs=None, + reverse=False, + run_mem_encoder=False, + prev_sam_mask_logits=prev_sam_mask_logits_singleton, + add_to_existing_state=False, + new_obj_idxs=singleton_obj_idxs, + new_obj_ids=singleton_obj_ids, + allow_new_buckets=False, + objects_to_interact=singleton_objects_to_interact, + ) + + singleton_storage_key = ( + "cond_frame_outputs" + if singleton_is_init_cond + else "non_cond_frame_outputs" + ) + + _, singleton_video_res_masks = self._get_orig_video_res_output( + singleton_state, current_out["pred_masks"] + ) + current_out["pred_masks_video_res"] = singleton_video_res_masks + + singleton_state["output_dict"][singleton_storage_key][frame_idx] = ( + current_out + ) + + self._merge_singleton_interaction_result( + inference_state, singleton_state, obj_id, original_obj_idx + ) + + obj_idx = inference_state["obj_id_to_idx"][obj_id] + obj_idxs = [obj_idx] + + if "user_refined_frames_per_obj" not in inference_state: + inference_state["user_refined_frames_per_obj"] = {} + if obj_id not in inference_state["user_refined_frames_per_obj"]: + inference_state["user_refined_frames_per_obj"][obj_id] = set() + + inference_state["user_refined_frames_per_obj"][obj_id].add(frame_idx) + + merged_frame_out = inference_state["output_dict"][singleton_storage_key][ + frame_idx + ] + obj_output_dict = inference_state["output_dict_per_obj"][obj_idx] + obj_temp_output_dict = inference_state["temp_output_dict_per_obj"][obj_idx] + + if "pred_masks_video_res" in merged_frame_out: + pred_masks_video_res_slice = merged_frame_out["pred_masks_video_res"][ + obj_idx : obj_idx + 1 + ] + else: + _, video_res_masks = self._get_orig_video_res_output( + inference_state, merged_frame_out["pred_masks"] + ) + pred_masks_video_res_slice = video_res_masks[obj_idx : obj_idx + 1] + + pred_masks_slice = merged_frame_out["pred_masks"][obj_idx : obj_idx + 1] + + obj_temp_output_dict[singleton_storage_key][frame_idx] = { + "pred_masks": pred_masks_slice, + "pred_masks_video_res": pred_masks_video_res_slice, + "object_score_logits": merged_frame_out["object_score_logits"][ + obj_idx : obj_idx + 1 + ], + } + obj_output_dict[singleton_storage_key][frame_idx] = obj_temp_output_dict[ + singleton_storage_key + ][frame_idx] + + elif is_gap_fill_case: + # Gap fill: Run inference directly in multiplex mode (no singleton extraction) + # Even though is_init_cond_frame=True, we use add_to_existing_state=False + # because the object ALREADY EXISTS in multiplex state. + obj_idx = inference_state["obj_id_to_idx"][obj_id] + obj_idxs = [obj_idx] + batch_size = self._get_obj_num(inference_state) + + obj_output_dict = inference_state["output_dict_per_obj"][obj_idx] + obj_temp_output_dict = inference_state["temp_output_dict_per_obj"][obj_idx] + + current_out, _ = self._run_single_frame_inference( + inference_state=inference_state, + output_dict=inference_state["output_dict"], + frame_idx=frame_idx, + batch_size=batch_size, + is_init_cond_frame=True, + point_inputs=point_inputs, + mask_inputs=None, + reverse=False, + run_mem_encoder=False, + prev_sam_mask_logits=None, + add_to_existing_state=False, + new_obj_idxs=[obj_idx], + new_obj_ids=[obj_id], + allow_new_buckets=False, + prefer_new_buckets=False, + objects_to_interact=[obj_idx], + ) + + current_out["local_obj_id_to_idx"] = deepcopy( + inference_state["obj_id_to_idx"] + ) + + _, video_res_masks = self._get_orig_video_res_output( + inference_state, current_out["pred_masks"] + ) + current_out["pred_masks_video_res"] = video_res_masks + + is_cond = storage_key == "cond_frame_outputs" + if ( + is_cond + and frame_idx + in inference_state["output_dict"]["non_cond_frame_outputs"] + ): + del inference_state["output_dict"]["non_cond_frame_outputs"][frame_idx] + if "consolidated_frame_inds" in inference_state: + inference_state["consolidated_frame_inds"][ + "non_cond_frame_outputs" + ].discard(frame_idx) + + # Store consolidated output (has obj_ptr, maskmem_features, etc.) + inference_state["output_dict"][storage_key][frame_idx] = current_out + + # Mark as consolidated + if "consolidated_frame_inds" in inference_state: + inference_state["consolidated_frame_inds"][storage_key].add(frame_idx) + + # Also store per-object slices in temp_output_dict_per_obj + obj_temp_output_dict[storage_key][frame_idx] = { + "pred_masks": current_out["pred_masks"][obj_idx : obj_idx + 1], + "pred_masks_video_res": video_res_masks[obj_idx : obj_idx + 1], + "object_score_logits": current_out["object_score_logits"][ + obj_idx : obj_idx + 1 + ], + } + obj_output_dict[storage_key][frame_idx] = obj_temp_output_dict[storage_key][ + frame_idx + ] + + # Store outputs and prepare return values + obj_output_dict = inference_state["output_dict_per_obj"][obj_idx] + obj_temp_output_dict = inference_state["temp_output_dict_per_obj"][obj_idx] + + # For refinement/gap fill (singleton extraction), handle singleton output specially + if is_refine or is_gap_fill_case: + # Singleton case: The merge already updated the consolidated output_dict during merge. + # However, we need to ensure the frame is properly stored and marked. + + singleton_obj_idx = 0 + + # Get video resolution masks from singleton output + _, video_res_masks_singleton = self._get_orig_video_res_output( + inference_state, current_out["pred_masks"] + ) + + # Mark frame as consolidated (prevents double consolidation in preflight) + if "consolidated_frame_inds" in inference_state: + inference_state["consolidated_frame_inds"][storage_key].add(frame_idx) + + # For return value, use singleton masks + video_res_masks_to_return = video_res_masks_singleton[ + singleton_obj_idx : singleton_obj_idx + 1 + ] + else: + # Standard multiplex output - use obj_idx + _, video_res_masks = self._get_orig_video_res_output( + inference_state, current_out["pred_masks"] + ) + + current_out["pred_masks_video_res"] = video_res_masks + current_out["local_obj_id_to_idx"] = deepcopy( + inference_state["obj_id_to_idx"] + ) + + # Remove from non_cond if this becomes a cond frame + if ( + is_cond + and frame_idx + in inference_state["output_dict"]["non_cond_frame_outputs"] + ): + del inference_state["output_dict"]["non_cond_frame_outputs"][frame_idx] + # Also update consolidated_frame_inds + if "consolidated_frame_inds" in inference_state: + inference_state["consolidated_frame_inds"][ + "non_cond_frame_outputs" + ].discard(frame_idx) + + inference_state["output_dict"][storage_key][frame_idx] = current_out + + # Update consolidated_frame_inds to track this frame + if "consolidated_frame_inds" in inference_state: + inference_state["consolidated_frame_inds"][storage_key].add(frame_idx) + + # Store per-object outputs (slice from the full multiplex output) + obj_temp_output_dict[storage_key][frame_idx] = { + "pred_masks_video_res": current_out["pred_masks_video_res"][ + obj_idx : obj_idx + 1 + ], + "pred_masks": current_out["pred_masks"][obj_idx : obj_idx + 1], + "object_score_logits": current_out["object_score_logits"][ + obj_idx : obj_idx + 1 + ], + } + + obj_output_dict[storage_key][frame_idx] = obj_temp_output_dict[storage_key][ + frame_idx + ] + + video_res_masks_to_return = video_res_masks[obj_idx : obj_idx + 1] + + low_res_masks = None + return frame_idx, obj_ids, low_res_masks, video_res_masks_to_return + + @torch.inference_mode() + def add_new_masks( + self, + inference_state, + frame_idx, + obj_ids, + masks, + # for compatibility with per_obj_inference class, not used here + add_mask_to_memory=False, + # for object reconditioning; do not update the multiplex state + reconditioning=False, + ): + """Add new mask to a frame.""" + if isinstance(obj_ids, np.ndarray): + obj_ids = obj_ids.tolist() + obj_idxs = [ + self._obj_id_to_idx(inference_state, obj_id, error_if_new=reconditioning) + for obj_id in obj_ids + ] + point_inputs_per_frame = [ + inference_state["point_inputs_per_obj"][obj_idx] for obj_idx in obj_idxs + ] + mask_inputs_per_frame = [ + inference_state["mask_inputs_per_obj"][obj_idx] for obj_idx in obj_idxs + ] + + assert masks.dim() == 3 + num_objects, mask_H, mask_W = masks.shape + assert num_objects == len(obj_ids) + masks_inputs_orig = masks[:, None, :, :] # add channel dimension + masks_inputs_orig = masks_inputs_orig.float().to(inference_state["device"]) + + # resize the mask if it doesn't match the model's input mask size + if mask_H != self.input_mask_size or mask_W != self.input_mask_size: + mask_inputs = torch.nn.functional.interpolate( + masks_inputs_orig, + size=(self.input_mask_size, self.input_mask_size), + align_corners=False, + mode="bilinear", + antialias=True, # use antialias for downsampling + ) + else: + mask_inputs = masks_inputs_orig + + # also get the mask at the original video resolution (for outputting) + video_H = inference_state["video_height"] + video_W = inference_state["video_width"] + if mask_H != video_H or mask_W != video_W: + mask_inputs_video_res = torch.nn.functional.interpolate( + masks_inputs_orig, + size=(video_H, video_W), + align_corners=False, + mode="bilinear", + antialias=True, # use antialias for potential downsampling + ) + else: + mask_inputs_video_res = masks_inputs_orig + # convert mask_inputs_video_res to binary (threshold at 0.5 as it is in range 0~1) + mask_inputs_video_res = mask_inputs_video_res > 0.5 + + multiplex_state = inference_state["multiplex_state"] + is_new_state = multiplex_state is None + + if not reconditioning: + if is_new_state: + multiplex_state = self.multiplex_controller.get_state( + num_valid_entries=num_objects, + device=inference_state["device"], + dtype=torch.float32, # lower precision is also fine + random=False, + object_ids=obj_ids, + ) + inference_state["multiplex_state"] = multiplex_state + else: + assert ( + self.is_dynamic_model + ), "New objects are not allowed after state creation" + + for i in range(num_objects): + mask_inputs_per_frame[i][frame_idx] = mask_inputs_video_res[i : i + 1] + point_inputs_per_frame[i].pop(frame_idx, None) + # If this frame hasn't been tracked before, we treat it as an initial conditioning + # frame, meaning that the inputs points are to generate segments on this frame without + # using any memory from other frames, like in SAM. Otherwise (if it has been tracked), + # the input points will be used to correct the already tracked masks. + is_init_cond_frame = frame_idx not in inference_state["frames_already_tracked"] + # whether to track in reverse time order + if is_init_cond_frame: + reverse = False + else: + reverse = inference_state["frames_already_tracked"][frame_idx]["reverse"] + obj_output_dicts = [ + inference_state["output_dict_per_obj"][obj_idx] for obj_idx in obj_idxs + ] + obj_temp_output_dicts = [ + inference_state["temp_output_dict_per_obj"][obj_idx] for obj_idx in obj_idxs + ] + # Add a frame to conditioning output if it's an initial conditioning frame or + # if the model sees all frames receiving clicks/mask as conditioning frames. + is_cond = is_init_cond_frame or self.add_all_frames_to_correct_as_cond + storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs" + + # Allow creating a new bucket only when existing buckets cannot fit the new objects + allow_new_buckets_local = False + if not is_new_state and not reconditioning and multiplex_state is not None: + if multiplex_state.available_slots < num_objects: + allow_new_buckets_local = True + + current_out, _ = self._run_single_frame_inference( + inference_state=inference_state, + output_dict=inference_state["output_dict"], + frame_idx=frame_idx, + batch_size=num_objects, + is_init_cond_frame=is_init_cond_frame, + point_inputs=None, + mask_inputs=mask_inputs, + reverse=reverse, + # Skip the memory encoder when adding clicks or mask. We execute the memory encoder + # at the beginning of `propagate_in_video` (after user finalize their clicks). This + # allows us to enforce non-overlapping constraints on all objects before encoding + # them into memory. + run_mem_encoder=False, + add_to_existing_state=not is_new_state and not reconditioning, + new_obj_idxs=obj_idxs, + new_obj_ids=obj_ids, + allow_new_buckets=allow_new_buckets_local, + reconditioning=reconditioning, + ) + # We directly use the input mask at video resolution as the output mask for a better + # video editing experience (so that the masks don't change after each brushing). + # Here NO_OBJ_SCORE is a large negative value to represent the background and + # similarly -NO_OBJ_SCORE is a large positive value to represent the foreground. + _, video_res_masks = self._get_orig_video_res_output( + inference_state, current_out["pred_masks"] + ) + obj_idxs_t = torch.as_tensor(obj_idxs, device=video_res_masks.device) + video_res_masks[obj_idxs_t] = torch.where( + mask_inputs_video_res, -NO_OBJ_SCORE, NO_OBJ_SCORE + ) + + current_out["pred_masks_video_res"] = video_res_masks + with torch.profiler.record_function("add_new_masks._deepcopy"): + current_out["local_obj_id_to_idx"] = deepcopy( + inference_state["obj_id_to_idx"] + ) + if ( + is_cond + and frame_idx in inference_state["output_dict"]["non_cond_frame_outputs"] + ): + del inference_state["output_dict"]["non_cond_frame_outputs"][frame_idx] + # Also update consolidated_frame_inds + if "consolidated_frame_inds" in inference_state: + inference_state["consolidated_frame_inds"][ + "non_cond_frame_outputs" + ].discard(frame_idx) + + inference_state["output_dict"][storage_key][frame_idx] = current_out + + # Update consolidated_frame_inds to track this frame + if "consolidated_frame_inds" in inference_state: + inference_state["consolidated_frame_inds"][storage_key].add(frame_idx) + + with torch.profiler.record_function("add_new_masks.obj_loop"): + # Step 1: Set all new object masks first (batched) + for i, obj_idx in enumerate(obj_idxs): + # Add the predicted masks to the output dict + # NOTE: object ordering matters here but I guess this is the same for the per-object implementation + obj_temp_output_dicts[i][storage_key][frame_idx] = { + "pred_masks_video_res": current_out["pred_masks_video_res"][ + obj_idx : obj_idx + 1 + ] + } + obj_output_dicts[i][storage_key][frame_idx] = obj_temp_output_dicts[i][ + storage_key + ][frame_idx] + + # Step 2: Precompute suppress masks to avoid O(n*m) torch.where calls + # Combined mask of all new objects (for existing objects) + combined_new_mask = mask_inputs_video_res.any( + dim=0, keepdim=True + ) # (1, 1, H, W) + + # Precompute exclude-self masks for new objects (if there are multiple new objects) + num_new = len(obj_idxs) + exclude_self_masks = {} + if num_new > 1: + for i in range(num_new): + other_indices = torch.cat( + [ + torch.arange(i, device=mask_inputs_video_res.device), + torch.arange( + i + 1, num_new, device=mask_inputs_video_res.device + ), + ] + ) + exclude_self_masks[obj_idxs[i]] = mask_inputs_video_res[ + other_indices + ].any(dim=0, keepdim=True) + + # Step 3: Apply suppression to all objects in a single pass + temp_output_dict_per_obj = inference_state["temp_output_dict_per_obj"] + obj_idxs_set = set(obj_idxs) + + for obj_idx2, obj_temp_output_dict2 in temp_output_dict_per_obj.items(): + current_out2 = obj_temp_output_dict2[storage_key].get(frame_idx, None) + if current_out2 is None: + continue + + if obj_idx2 not in obj_idxs_set: + # Existing object: suppress by all new masks + suppress_mask = combined_new_mask + elif obj_idx2 in exclude_self_masks: + # New object: suppress by other new objects' masks + suppress_mask = exclude_self_masks[obj_idx2] + else: + # Only one new object - nothing to suppress for itself + continue + + current_out2["pred_masks_video_res"] = torch.where( + suppress_mask, + NO_OBJ_SCORE, + current_out2["pred_masks_video_res"], + ) + + # Resize the output mask to the original video resolution + obj_ids = inference_state["obj_ids"] + consolidated_out = self._consolidate_temp_output_across_obj( + inference_state, + frame_idx, + is_cond=is_cond, + run_mem_encoder=False, + consolidate_at_video_res=True, + ) + _, video_res_masks = self._get_orig_video_res_output( + inference_state, consolidated_out["pred_masks_video_res"] + ) + low_res_masks = None # not needed by the demo + + consolidated_out["local_obj_id_to_idx"] = current_out["local_obj_id_to_idx"] + + return frame_idx, obj_ids, low_res_masks, video_res_masks + + def _get_orig_video_res_output(self, inference_state, any_res_masks): + """ + Resize the object scores to the original video resolution (video_res_masks) + and apply non-overlapping constraints for final output. + """ + device = inference_state["device"] + video_H = inference_state["video_height"] + video_W = inference_state["video_width"] + any_res_masks = any_res_masks.to(device, non_blocking=True) + if any_res_masks.shape[-2:] == (video_H, video_W): + video_res_masks = any_res_masks + else: + video_res_masks = torch.nn.functional.interpolate( + any_res_masks, + size=(video_H, video_W), + mode="bilinear", + align_corners=False, + ) + if self.non_overlap_masks_for_output: + video_res_masks = self._apply_non_overlapping_constraints(video_res_masks) + # potentially fill holes in the predicted masks + if self.fill_hole_area > 0: + video_res_masks = fill_holes_in_mask_scores( + video_res_masks, self.fill_hole_area + ) + return any_res_masks, video_res_masks + + def _consolidate_temp_output_across_obj( + self, + inference_state, + frame_idx, + is_cond, + run_mem_encoder, + consolidate_at_video_res=False, + ): + """ + Consolidate the per-object temporary outputs in `temp_output_dict_per_obj` on + a frame into a single output for all objects, including + 1) fill any missing objects either from `output_dict_per_obj` (if they exist in + `output_dict_per_obj` for this frame) or leave them as placeholder values + (if they don't exist in `output_dict_per_obj` for this frame); + 2) if specified, rerun memory encoder after apply non-overlapping constraints + on the object scores. + """ + batch_size = self._get_obj_num(inference_state) + storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs" + + # After singleton merge, objects can be added at indices beyond batch_size + # We need to find the maximum object index that has temp or regular outputs to size the tensor correctly + max_obj_idx = batch_size - 1 # Default to batch_size - 1 + + # Check both temp and regular output dicts to find max index + for obj_idx in inference_state["temp_output_dict_per_obj"].keys(): + if obj_idx > max_obj_idx: + max_obj_idx = obj_idx + for obj_idx in inference_state["output_dict_per_obj"].keys(): + if obj_idx > max_obj_idx: + max_obj_idx = obj_idx + + # Size the consolidated tensor to accommodate all object indices (not just count) + consolidated_batch_size = max(max_obj_idx + 1, 0) # Ensure non-negative + + # Optionally, we allow consolidating the temporary outputs at the original + # video resolution (to provide a better editing experience for mask prompts). + if consolidate_at_video_res: + assert not run_mem_encoder, "memory encoder cannot run at video resolution" + consolidated_H = inference_state["video_height"] + consolidated_W = inference_state["video_width"] + consolidated_mask_key = "pred_masks_video_res" + else: + consolidated_H = consolidated_W = self.low_res_mask_size + consolidated_mask_key = "pred_masks" + + # Initialize `consolidated_out`. Its "maskmem_features" and "maskmem_pos_enc" + # will be added when rerunning the memory encoder after applying non-overlapping + # constraints to object scores. Its "pred_masks" are prefilled with a large + # negative value (NO_OBJ_SCORE) to represent missing objects. + + consolidated_out = { + "conditioning_objects": None, + "maskmem_features": None, + "maskmem_pos_enc": None, + "image_features": None, + "image_pos_enc": None, + "obj_ptr": None, + consolidated_mask_key: torch.full( + size=( + consolidated_batch_size, + 1, + consolidated_H, + consolidated_W, + ), # Use consolidated_batch_size, not batch_size! + fill_value=NO_OBJ_SCORE, + dtype=torch.float32, + device=inference_state["storage_device"], + ), + } + + all_out = inference_state["output_dict"]["cond_frame_outputs"].get( + frame_idx, None + ) + if all_out is None: + all_out = inference_state["output_dict"]["non_cond_frame_outputs"].get( + frame_idx, None + ) + + # Handle the case where output_dict is empty (e.g., during demo VG propagation) + # In this case, we'll reconstruct the consolidated output from per-object outputs + need_to_reconstruct_from_per_obj = all_out is None + + if need_to_reconstruct_from_per_obj: + # Initialize fields that will be populated from per-object outputs or later + # Determine which objects are conditioned by checking if they have point/mask inputs on this frame + conditioning_objects = set() + for obj_idx in range(batch_size): + # Check if this object has point inputs on this frame + if obj_idx in inference_state["point_inputs_per_obj"]: + point_inputs = inference_state["point_inputs_per_obj"][obj_idx] + if ( + frame_idx in point_inputs + and point_inputs[frame_idx] is not None + ): + conditioning_objects.add(obj_idx) + continue + + # Check if this object has mask inputs on this frame + if obj_idx in inference_state["mask_inputs_per_obj"]: + mask_inputs = inference_state["mask_inputs_per_obj"][obj_idx] + if frame_idx in mask_inputs and mask_inputs[frame_idx] is not None: + conditioning_objects.add(obj_idx) + + consolidated_out["conditioning_objects"] = conditioning_objects + # Shared features will be populated when running memory encoder + # Note: obj_ptr and object_score_logits will be populated from per-object outputs below + else: + # Normal case: populate from existing consolidated output + consolidated_out["conditioning_objects"] = all_out.get( + "conditioning_objects", set() + ) + consolidated_out["obj_ptr"] = all_out["obj_ptr"] + consolidated_out["object_score_logits"] = all_out["object_score_logits"] + if self.use_memory_selection: + consolidated_out["iou_score"] = all_out["iou_score"] + # These fields might not exist in per-object outputs (e.g., after singleton extraction) + consolidated_out["maskmem_features"] = all_out.get("maskmem_features") + consolidated_out["maskmem_pos_enc"] = all_out.get("maskmem_pos_enc") + consolidated_out["image_features"] = all_out.get("image_features") + consolidated_out["image_pos_enc"] = all_out.get("image_pos_enc") + consolidated_out["local_obj_id_to_idx"] = all_out.get( + "local_obj_id_to_idx", {} + ) + consolidated_out["obj_ptr"] = all_out["obj_ptr"] + consolidated_out["object_score_logits"] = all_out["object_score_logits"] + if self.use_memory_selection: + consolidated_out["iou_score"] = all_out["iou_score"] + # These fields might not exist in per-object outputs (e.g., after singleton extraction) + consolidated_out["maskmem_features"] = all_out.get("maskmem_features") + consolidated_out["maskmem_pos_enc"] = all_out.get("maskmem_pos_enc") + consolidated_out["image_features"] = all_out.get("image_features") + consolidated_out["image_pos_enc"] = all_out.get("image_pos_enc") + consolidated_out["local_obj_id_to_idx"] = all_out.get( + "local_obj_id_to_idx", {} + ) + all_mask = all_out.get("pred_masks_video_res", all_out["pred_masks"]) + # Ensure masks are at the correct consolidated resolution + # This handles the case where all_out has interactive resolution (288) masks + # that need to be resized to SAM2's low_res_mask_size (256) for consistency + if all_mask.shape[-2:] == (consolidated_H, consolidated_W): + consolidated_out[consolidated_mask_key] = all_mask + else: + # Resize first if mask has a different resolution (e.g., 288 from interactive) + # Determine if we're downsampling or upsampling + is_downsampling = all_mask.shape[-1] > consolidated_W + resized_mask = torch.nn.functional.interpolate( + all_mask, + size=(consolidated_H, consolidated_W), + mode="bilinear", + align_corners=False, + antialias=is_downsampling, # use antialias for downsampling + ) + consolidated_out[consolidated_mask_key] = resized_mask + + # Collect per-object outputs (masks and scores) to build consolidated output + # When reconstructing from per-object outputs, we also need to collect obj_ptr and object_score_logits + obj_score_logits_list = [] + obj_ptr_list = [] if need_to_reconstruct_from_per_obj else None + iou_scores_list = ( + [] + if need_to_reconstruct_from_per_obj and self.use_memory_selection + else None + ) + + # When reconstructing from per-object outputs, initialize the mask tensor + # with the correct size (consolidated_batch_size, not batch_size) + if ( + need_to_reconstruct_from_per_obj + and consolidated_mask_key not in consolidated_out + ): + # Initialize with zeros - will be populated from per-object outputs below + consolidated_out[consolidated_mask_key] = torch.zeros( + (consolidated_batch_size, 1, consolidated_H, consolidated_W), + dtype=torch.float32, + device=inference_state["storage_device"], + ) + consolidated_out["object_score_logits"] = torch.full( + (consolidated_batch_size, 1), + NO_OBJ_SCORE, + dtype=torch.float32, + device=inference_state["storage_device"], + ) + + for obj_idx in range( + consolidated_batch_size + ): # Use consolidated_batch_size instead of batch_size + # Check if this object index exists in temp/output dicts (it may not if object was just added) + if obj_idx not in inference_state["temp_output_dict_per_obj"]: + continue + if obj_idx not in inference_state["output_dict_per_obj"]: + continue + obj_temp_output_dict = inference_state["temp_output_dict_per_obj"][obj_idx] + obj_output_dict = inference_state["output_dict_per_obj"][obj_idx] + out = obj_temp_output_dict[storage_key].get(frame_idx, None) + # If the object doesn't appear in "temp_output_dict_per_obj" on this frame, + # we fall back and look up its previous output in "output_dict_per_obj". + # We look up both "cond_frame_outputs" and "non_cond_frame_outputs" in + # "output_dict_per_obj" to find a previous output for this object. + if out is None: + out = obj_output_dict["cond_frame_outputs"].get(frame_idx, None) + if out is None: + out = obj_output_dict["non_cond_frame_outputs"].get(frame_idx, None) + if out is None: + # object pointers are filled globally above; we don't need empty_mask_ptr + continue + # Add the temporary object output mask to consolidated output mask + # (use "pred_masks_video_res" if it's available) + obj_mask = out.get("pred_masks_video_res") + if obj_mask is None: + obj_mask = out.get("pred_masks") + consolidated_pred_masks = consolidated_out[consolidated_mask_key] + + # If obj_idx is beyond the consolidated_pred_masks size, + # we need to expand it (can happen after singleton merge adds object at end) + if obj_idx >= consolidated_pred_masks.shape[0]: + pad_size = obj_idx + 1 - consolidated_pred_masks.shape[0] + consolidated_pred_masks = torch.cat( + [ + consolidated_pred_masks, + torch.zeros( + ( + pad_size, + 1, + consolidated_pred_masks.shape[-2], + consolidated_pred_masks.shape[-1], + ), + dtype=consolidated_pred_masks.dtype, + device=consolidated_pred_masks.device, + ), + ], + dim=0, + ) + consolidated_out[consolidated_mask_key] = consolidated_pred_masks + # Also expand object_score_logits if present + if "object_score_logits" in consolidated_out: + consolidated_scores = consolidated_out["object_score_logits"] + consolidated_scores = torch.cat( + [ + consolidated_scores, + torch.full( + (pad_size, 1), + NO_OBJ_SCORE, + dtype=consolidated_scores.dtype, + device=consolidated_scores.device, + ), + ], + dim=0, + ) + consolidated_out["object_score_logits"] = consolidated_scores + + if obj_mask.shape[-2:] == consolidated_pred_masks.shape[-2:]: + # Ensure dtype match between source and destination before assignment + if obj_mask.dtype != consolidated_pred_masks.dtype: + obj_mask = obj_mask.to(consolidated_pred_masks.dtype) + consolidated_pred_masks[obj_idx : obj_idx + 1] = obj_mask + else: + # Resize first if temporary object mask has a different resolution + is_downsampling = "pred_masks_video_res" in out + resized_obj_mask = torch.nn.functional.interpolate( + obj_mask, + size=consolidated_pred_masks.shape[-2:], + mode="bilinear", + align_corners=False, + antialias=is_downsampling, # use antialias for downsampling + ) + # Ensure dtype match between source and destination before assignment + if resized_obj_mask.dtype != consolidated_pred_masks.dtype: + resized_obj_mask = resized_obj_mask.to( + consolidated_pred_masks.dtype + ) + consolidated_pred_masks[obj_idx : obj_idx + 1] = resized_obj_mask + + # When reconstructing from per-object outputs, also collect scores + if need_to_reconstruct_from_per_obj: + if "object_score_logits" in out: + obj_score_logits_list.append(out["object_score_logits"]) + if self.use_memory_selection and "iou_score" in out: + iou_scores_list.append(out["iou_score"]) + + # If we reconstructed from per-object outputs, consolidate the score fields + if need_to_reconstruct_from_per_obj: + # Check if we have ANY valid per-object outputs + # If not, we're trying to consolidate a VG-propagated frame that was never + # stored in output_dict (only in cached_frame_outputs) + # In this case, we SKIP memory encoding during preflight and will do it + # during the first propagation step instead + if not obj_score_logits_list and run_mem_encoder: + run_mem_encoder = False # Skip for now, will encode during propagation + + if obj_score_logits_list: + consolidated_out["object_score_logits"] = torch.cat( + obj_score_logits_list, dim=0 + ) + else: + # Create placeholder scores - these will be replaced when memory encoder runs + device = inference_state["device"] + consolidated_out["object_score_logits"] = torch.zeros( + (batch_size, 1), + dtype=torch.float32, + device=device, + ) + + if self.use_memory_selection: + if iou_scores_list: + consolidated_out["iou_score"] = torch.cat(iou_scores_list, dim=0) + else: + consolidated_out["iou_score"] = None + + # obj_ptr will be populated by memory encoder, set to None for now + consolidated_out["obj_ptr"] = None + + # Optionally, apply non-overlapping constraints on the consolidated scores + # and rerun the memory encoder + if run_mem_encoder: + device = inference_state["device"] + high_res_masks = torch.nn.functional.interpolate( + consolidated_out["pred_masks"].to(device, non_blocking=True), + size=(self.image_size, self.image_size), + mode="bilinear", + align_corners=False, + ) + high_res_masks = self._apply_non_overlapping_constraints(high_res_masks) + maskmem_features, maskmem_pos_enc, image_features, image_pos_enc = ( + self._run_memory_encoder( + inference_state=inference_state, + frame_idx=frame_idx, + batch_size=batch_size, + high_res_masks=high_res_masks, + object_score_logits=consolidated_out["object_score_logits"], + is_mask_from_pts=True, # these frames are what the user interacted with + conditioning_objects=consolidated_out[ + "conditioning_objects" + ], # Pass conditioning_objects + ) + ) + consolidated_out["maskmem_features"] = maskmem_features + consolidated_out["maskmem_pos_enc"] = maskmem_pos_enc + consolidated_out["image_features"] = image_features + consolidated_out["image_pos_enc"] = image_pos_enc + + return consolidated_out + + @torch.inference_mode() + def propagate_in_video_preflight(self, inference_state, run_mem_encoder=True): + """Prepare inference_state and consolidate temporary outputs before tracking.""" + inference_state["tracking_has_started"] = True + batch_size = self._get_obj_num(inference_state) + + # Consolidate per-object temporary outputs in "temp_output_dict_per_obj" and + # add them into "output_dict". + temp_output_dict_per_obj = inference_state["temp_output_dict_per_obj"] + output_dict = inference_state["output_dict"] + # "consolidated_frame_inds" contains indices of those frames where consolidated + # temporary outputs have been added (either in this call or any previous calls + # to `propagate_in_video_preflight`). + consolidated_frame_inds = inference_state["consolidated_frame_inds"] + for is_cond in [False, True]: + # Separately consolidate conditioning and non-conditioning temp outptus + storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs" + # Find all the frames that contain temporary outputs for any objects + # (these should be the frames that have just received clicks for mask inputs + # via `add_new_points` or `add_new_mask`) + temp_frame_inds = set() + for obj_temp_output_dict in temp_output_dict_per_obj.values(): + temp_frame_inds.update(obj_temp_output_dict[storage_key].keys()) + consolidated_frame_inds[storage_key].update(temp_frame_inds) + # consolidate the temprary output across all objects on this frame + for frame_idx in temp_frame_inds: + consolidated_out = self._consolidate_temp_output_across_obj( + inference_state, + frame_idx, + is_cond=is_cond, + run_mem_encoder=run_mem_encoder, + ) + # merge them into "output_dict" and also create per-object slices + output_dict[storage_key][frame_idx] = consolidated_out + self._add_output_per_object( + inference_state, frame_idx, consolidated_out, storage_key + ) + clear_non_cond_mem = self.clear_non_cond_mem_around_input and ( + self.clear_non_cond_mem_for_multi_obj or batch_size <= 1 + ) + if clear_non_cond_mem: + # clear non-conditioning memory of the surrounding frames + self._clear_non_cond_mem_around_input(inference_state, frame_idx) + + # clear temporary outputs in `temp_output_dict_per_obj` + for obj_temp_output_dict in temp_output_dict_per_obj.values(): + obj_temp_output_dict[storage_key].clear() + + # edge case: if an output is added to "cond_frame_outputs", we remove any prior + # output on the same frame in "non_cond_frame_outputs" + for frame_idx in output_dict["cond_frame_outputs"]: + output_dict["non_cond_frame_outputs"].pop(frame_idx, None) + for obj_output_dict in inference_state["output_dict_per_obj"].values(): + for frame_idx in obj_output_dict["cond_frame_outputs"]: + obj_output_dict["non_cond_frame_outputs"].pop(frame_idx, None) + for frame_idx in consolidated_frame_inds["cond_frame_outputs"]: + assert frame_idx in output_dict["cond_frame_outputs"] + consolidated_frame_inds["non_cond_frame_outputs"].discard(frame_idx) + + # Make sure that the frame indices in "consolidated_frame_inds" are exactly those frames + # with either points or mask inputs (which should be true under a correct demo workflow). + all_consolidated_frame_inds = ( + consolidated_frame_inds["cond_frame_outputs"] + | consolidated_frame_inds["non_cond_frame_outputs"] + ) + + input_frames_inds = set() + for point_inputs_per_frame in inference_state["point_inputs_per_obj"].values(): + input_frames_inds.update(point_inputs_per_frame.keys()) + for mask_inputs_per_frame in inference_state["mask_inputs_per_obj"].values(): + input_frames_inds.update(mask_inputs_per_frame.keys()) + assert all_consolidated_frame_inds == input_frames_inds + # Record the first interacted frame index (for tracking start) + if inference_state["first_ann_frame_idx"] is None: + inference_state["first_ann_frame_idx"] = min( + input_frames_inds, default=None + ) + # In case `first_ann_frame_idx` is not in the conditioning frames (e.g. because + # we cleared the input points on that frame), pick the first conditioning frame + if ( + inference_state["first_ann_frame_idx"] + not in output_dict["cond_frame_outputs"] + ): + inference_state["first_ann_frame_idx"] = min( + output_dict["cond_frame_outputs"], default=None + ) + + def _get_processing_order( + self, inference_state, start_frame_idx, max_frame_num_to_track, reverse + ): + num_frames = inference_state["num_frames"] + # set start index, end index, and processing order + if self.always_start_from_first_ann_frame: + # in this case, we always start tracking from the frame where we receive + # the initial annotation and ignore the provided start_frame_idx + start_frame_idx = inference_state["first_ann_frame_idx"] + if start_frame_idx is None: + # default: start from the earliest frame with input points + start_frame_idx = min(inference_state["output_dict"]["cond_frame_outputs"]) + if max_frame_num_to_track is None: + # default: track all the frames in the video + max_frame_num_to_track = num_frames + if reverse: + end_frame_idx = max(start_frame_idx - max_frame_num_to_track, 0) + if start_frame_idx > 0: + processing_order = range(start_frame_idx, end_frame_idx - 1, -1) + else: + # TODO: Jie - this is the edge case that we start from frame 0 and track in reverse order; + # and in the case we track a single frame for dense tracking, it should still run 1 frame (idx=0). + # Not sure if this has any side effect. + # processing_order = [] # skip reverse tracking if starting from frame 0 <-- original behaviour + processing_order = [0] + else: + end_frame_idx = min( + start_frame_idx + max_frame_num_to_track, num_frames - 1 + ) + processing_order = range(start_frame_idx, end_frame_idx + 1) + return processing_order + + @torch.inference_mode() + def propagate_in_video( + self, + inference_state, + start_frame_idx, + max_frame_num_to_track, + reverse, + tqdm_disable=False, + obj_ids=None, + run_mem_encoder=True, + ): + """Propagate the input points across frames to track in the entire video.""" + output_dict = inference_state["output_dict"] + consolidated_frame_inds = inference_state["consolidated_frame_inds"] + if obj_ids is not None: + raise NotImplementedError( + "Per-object tracking yet for batched inference if not implemented." + ) + obj_ids = inference_state["obj_ids"] + batch_size = self._get_obj_num(inference_state) + if len(output_dict["cond_frame_outputs"]) == 0: + raise RuntimeError("No points are provided; please add points first") + clear_non_cond_mem = self.clear_non_cond_mem_around_input and ( + self.clear_non_cond_mem_for_multi_obj or batch_size <= 1 + ) + assert clear_non_cond_mem is False, "Not implemented" + + processing_order = self._get_processing_order( + inference_state, + start_frame_idx, + max_frame_num_to_track, + reverse, + ) + + for frame_idx in tqdm( + processing_order, desc="propagate in video", disable=tqdm_disable + ): + # We skip those frames already in consolidated outputs (these are frames + # that received input clicks or mask). Note that we cannot directly run + # batched forward on them via `_run_single_frame_inference` because the + # number of clicks on each object might be different. + if frame_idx in consolidated_frame_inds["cond_frame_outputs"]: + storage_key = "cond_frame_outputs" + current_out = output_dict[storage_key][frame_idx] + pred_masks = current_out["pred_masks"] + if clear_non_cond_mem: + # clear non-conditioning memory of the surrounding frames + self._clear_non_cond_mem_around_input(inference_state, frame_idx) + elif frame_idx in consolidated_frame_inds["non_cond_frame_outputs"]: + storage_key = "non_cond_frame_outputs" + current_out = output_dict[storage_key][frame_idx] + pred_masks = current_out["pred_masks"] + else: + storage_key = "non_cond_frame_outputs" + with torch.profiler.record_function( + "VideoTrackingMultiplexDemo._run_single_frame_inference" + ): + current_out, pred_masks = self._run_single_frame_inference( + inference_state=inference_state, + output_dict=output_dict, + frame_idx=frame_idx, + batch_size=batch_size, + is_init_cond_frame=False, + point_inputs=None, + mask_inputs=None, + reverse=reverse, + run_mem_encoder=run_mem_encoder, + ) + current_out["local_obj_id_to_idx"] = deepcopy( + inference_state["obj_id_to_idx"] + ) + output_dict[storage_key][frame_idx] = current_out + # Create slices of per-object outputs for subsequent interaction with each + # individual object after tracking. + self._add_output_per_object( + inference_state, frame_idx, current_out, storage_key + ) + inference_state["frames_already_tracked"][frame_idx] = {"reverse": reverse} + + # Resize the output mask to the original video resolution (we directly use + # the mask scores on GPU for output to avoid any CPU conversion in between) + low_res_masks, video_res_masks = self._get_orig_video_res_output( + inference_state, pred_masks + ) + yield frame_idx, obj_ids, low_res_masks, video_res_masks + + def _add_output_per_object( + self, inference_state, frame_idx, current_out, storage_key + ): + """ + Split a multi-object output into per-object output slices and add them into + `output_dict_per_obj`. The resulting slices share the same tensor storage. + """ + # Note for the multiplex model: we don't store the maskmem features + # because we don't use the memory during interaction + + output_dict_per_obj = inference_state["output_dict_per_obj"] + for obj_idx, obj_output_dict in output_dict_per_obj.items(): + obj_slice = slice(obj_idx, obj_idx + 1) + obj_out = { + "pred_masks": current_out["pred_masks"][obj_slice], + "object_score_logits": current_out["object_score_logits"][obj_slice], + } + if self.use_memory_selection: + obj_out["iou_score"] = current_out["iou_score"][obj_slice] + obj_output_dict[storage_key][frame_idx] = obj_out + + @torch.inference_mode() + def clear_all_points_in_frame( + self, + inference_state, + frame_idx, + obj_id, + need_output=True, + preserve_user_refined: bool = False, + ): + """Remove all input points or mask in a specific frame for a given object.""" + obj_idx = self._obj_id_to_idx(inference_state, obj_id) + + # Clear the conditioning information on the given frame + inference_state["point_inputs_per_obj"][obj_idx].pop(frame_idx, None) + inference_state["mask_inputs_per_obj"][obj_idx].pop(frame_idx, None) + + # Clear user refinement tracking for this frame and object unless preserving it + if ( + not preserve_user_refined + and "user_refined_frames_per_obj" in inference_state + ): + user_refined_map = inference_state["user_refined_frames_per_obj"] + if obj_id in user_refined_map: + user_refined_map[obj_id].discard(frame_idx) + + temp_output_dict_per_obj = inference_state["temp_output_dict_per_obj"] + temp_output_dict_per_obj[obj_idx]["cond_frame_outputs"].pop(frame_idx, None) + temp_output_dict_per_obj[obj_idx]["non_cond_frame_outputs"].pop(frame_idx, None) + + # Check and see if there are still any inputs left on this frame + batch_size = self._get_obj_num(inference_state) + frame_has_input = False + for obj_idx2 in range(batch_size): + # Skip if this object doesn't exist in the input dictionaries + if obj_idx2 not in inference_state["point_inputs_per_obj"]: + continue + if obj_idx2 not in inference_state["mask_inputs_per_obj"]: + continue + if frame_idx in inference_state["point_inputs_per_obj"][obj_idx2]: + frame_has_input = True + break + if frame_idx in inference_state["mask_inputs_per_obj"][obj_idx2]: + frame_has_input = True + break + + # If this frame has no remaining inputs for any objects, we further clear its + # conditioning frame status + if not frame_has_input: + output_dict = inference_state["output_dict"] + consolidated_frame_inds = inference_state["consolidated_frame_inds"] + consolidated_frame_inds["cond_frame_outputs"].discard(frame_idx) + consolidated_frame_inds["non_cond_frame_outputs"].discard(frame_idx) + # Remove the frame's conditioning output (possibly downgrading it to non-conditioning) + out = output_dict["cond_frame_outputs"].pop(frame_idx, None) + if out is not None: + # The frame is not a conditioning frame anymore since it's not receiving inputs, + # so we "downgrade" its output (if exists) to a non-conditioning frame output. + output_dict["non_cond_frame_outputs"][frame_idx] = out + inference_state["frames_already_tracked"].pop(frame_idx, None) + # Similarly, do it for the sliced output on each object. + for obj_idx2 in range(batch_size): + # Skip if this object doesn't exist in the output dictionary + if obj_idx2 not in inference_state["output_dict_per_obj"]: + continue + obj_output_dict = inference_state["output_dict_per_obj"][obj_idx2] + obj_out = obj_output_dict["cond_frame_outputs"].pop(frame_idx, None) + if obj_out is not None: + obj_output_dict["non_cond_frame_outputs"][frame_idx] = obj_out + + # If all the conditioning frames have been removed, we also clear the tracking outputs + if len(output_dict["cond_frame_outputs"]) == 0: + self._reset_tracking_results(inference_state) + + if not need_output: + return + # Finally, output updated masks per object (after removing the inputs above) + obj_ids = inference_state["obj_ids"] + is_cond = any( + frame_idx in obj_temp_output_dict["cond_frame_outputs"] + for obj_temp_output_dict in temp_output_dict_per_obj.values() + ) + consolidated_out = self._consolidate_temp_output_across_obj( + inference_state, + frame_idx, + is_cond=is_cond, + run_mem_encoder=False, + consolidate_at_video_res=True, + ) + _, video_res_masks = self._get_orig_video_res_output( + inference_state, consolidated_out["pred_masks_video_res"] + ) + low_res_masks = None # not needed by the demo + return frame_idx, obj_ids, low_res_masks, video_res_masks + + @torch.inference_mode() + def clear_all_points_in_video(self, inference_state): + """Remove all input points or mask in all frames throughout the video.""" + self._reset_tracking_results(inference_state) + # Remove all object ids + inference_state["obj_id_to_idx"].clear() + inference_state["obj_idx_to_id"].clear() + inference_state["obj_ids"].clear() + inference_state["point_inputs_per_obj"].clear() + inference_state["mask_inputs_per_obj"].clear() + inference_state["output_dict_per_obj"].clear() + inference_state["temp_output_dict_per_obj"].clear() + inference_state["multiplex_state"] = None + + def _reset_tracking_results(self, inference_state): + """Reset all tracking inputs and results across the videos.""" + for v in inference_state["point_inputs_per_obj"].values(): + v.clear() + for v in inference_state["mask_inputs_per_obj"].values(): + v.clear() + for v in inference_state["output_dict_per_obj"].values(): + v["cond_frame_outputs"].clear() + v["non_cond_frame_outputs"].clear() + for v in inference_state["temp_output_dict_per_obj"].values(): + v["cond_frame_outputs"].clear() + v["non_cond_frame_outputs"].clear() + inference_state["output_dict"]["cond_frame_outputs"].clear() + inference_state["output_dict"]["non_cond_frame_outputs"].clear() + inference_state["consolidated_frame_inds"]["cond_frame_outputs"].clear() + inference_state["consolidated_frame_inds"]["non_cond_frame_outputs"].clear() + inference_state["tracking_has_started"] = False + inference_state["frames_already_tracked"].clear() + inference_state["first_ann_frame_idx"] = None + + def _get_image_feature(self, inference_state, frame_idx, batch_size): + """Compute the image features on a given frame.""" + # Look up in the cache first + image, backbone_out = inference_state["cached_features"].get( + frame_idx, (None, None) + ) + if backbone_out is None: + # Cache miss -- we will run inference on a single image + image = inference_state["images"][frame_idx].cuda().float().unsqueeze(0) + # TODO: We should optimize this because we don't always need all three outs + backbone_out = self.forward_image( + NestedTensor(tensors=image, mask=None), + need_sam3_out=True, + need_interactive_out=True, + need_propagation_out=True, + ) + # Cache the most recent frame's feature (for repeated interactions with + # a frame; we can use an LRU cache for more frames in the future). + inference_state["cached_features"] = {frame_idx: (image, backbone_out)} + + features = self._prepare_backbone_features(backbone_out) + return image, features + + def _run_single_frame_inference( + self, + inference_state, + output_dict, + frame_idx, + batch_size, + is_init_cond_frame, + point_inputs, + mask_inputs, + reverse, + run_mem_encoder, + prev_sam_mask_logits=None, + add_to_existing_state: bool = False, + new_obj_idxs: Optional[list[int]] = None, + new_obj_ids: Optional[list[int]] = None, + allow_new_buckets: bool = False, + prefer_new_buckets: bool = False, + reconditioning: bool = False, + objects_to_interact: Optional[list[int]] = None, + ): + """Run tracking on a single frame based on current inputs and previous memory.""" + # Retrieve correct image features + with torch.profiler.record_function( + "VideoTrackingMultiplexDemo._get_image_feature" + ): + image, backbone_features = self._get_image_feature( + inference_state, frame_idx, batch_size + ) + + if add_to_existing_state or reconditioning: + assert new_obj_idxs is not None + assert new_obj_ids is not None + + backbone_features_interactive = backbone_features["interactive"] + backbone_features_propagation = backbone_features["sam2_backbone_out"] + + if add_to_existing_state or reconditioning: + with torch.profiler.record_function( + "VideoTrackingMultiplexDemo.add_new_masks_to_existing_state" + ): + # Get existing output from current frame to modify in-place + # Try both storage keys since the output could be in either location + existing_out = output_dict["cond_frame_outputs"].get(frame_idx) + if existing_out is None: + existing_out = output_dict["non_cond_frame_outputs"].get(frame_idx) + if existing_out is None: + raise RuntimeError( + f"No existing output found for frame {frame_idx} in either storage" + ) + + # Prepare interactive features + interactive_pix_feat = self._get_interactive_pix_mem( + backbone_features_interactive["vision_feats"], + backbone_features_interactive["feat_sizes"], + ) + + # High-resolution feature maps for the SAM head, reshape (HW)BC => BCHW + interactive_high_res_features = [ + x.permute(1, 2, 0).view(x.size(1), x.size(2), *s) + for x, s in zip( + backbone_features_interactive["vision_feats"][:-1], + backbone_features_interactive["feat_sizes"][:-1], + ) + ] + + # Prepare propagation features for memory encoding + propagation_vision_feats = ( + backbone_features_propagation["vision_feats"] + if run_mem_encoder + else None + ) + propagation_feat_sizes = ( + backbone_features_propagation["feat_sizes"] + if run_mem_encoder + else None + ) + + # Add new masks to existing state + if reconditioning: + self.recondition_masks_in_existing_state( + interactive_pix_feat=interactive_pix_feat, + interactive_high_res_features=interactive_high_res_features, + propagation_vision_feats=propagation_vision_feats, + propagation_feat_sizes=propagation_feat_sizes, + new_masks=mask_inputs, + obj_idxs_in_mask=new_obj_idxs, + obj_ids_in_mask=new_obj_ids, + prev_output=existing_out, + multiplex_state=inference_state["multiplex_state"], + add_mask_to_memory=run_mem_encoder, + ) + else: + # If we are adding to existing state using points (mask_inputs is None), + # first convert points -> masks via the interactivity head. + new_masks_from_points = None + if mask_inputs is None and point_inputs is not None: + with torch.profiler.record_function( + "VideoTrackingMultiplexDemo.points_to_masks" + ): + multimask_output = self._use_multimask( + is_init_cond_frame, point_inputs=point_inputs + ) + interaction_out = self._forward_sam_heads( + backbone_features=interactive_pix_feat, + point_inputs=point_inputs, + mask_inputs=None, + interactive_high_res_features=interactive_high_res_features, + multimask_output=multimask_output, + objects_to_interact=new_obj_idxs, + multiplex_state=inference_state["multiplex_state"], + ) + new_masks_from_points = interaction_out["low_res_masks"] + + self.add_new_masks_to_existing_state( + interactive_pix_feat=interactive_pix_feat, + interactive_high_res_features=interactive_high_res_features, + propagation_vision_feats=propagation_vision_feats, + propagation_feat_sizes=propagation_feat_sizes, + new_masks=( + mask_inputs + if mask_inputs is not None + else new_masks_from_points + ), + obj_idxs_in_mask=new_obj_idxs, + obj_ids_in_mask=new_obj_ids, + prev_output=existing_out, + multiplex_state=inference_state["multiplex_state"], + add_mask_to_memory=run_mem_encoder, + are_masks_from_pts=(mask_inputs is None), + allow_new_buckets=allow_new_buckets, + prefer_new_buckets=prefer_new_buckets, + ) + + # Return the modified existing output + current_out = existing_out + else: + # point and mask should not appear as input simultaneously on the same frame + assert point_inputs is None or mask_inputs is None + with torch.profiler.record_function( + "VideoTrackingMultiplexDemo.track_step" + ): + current_out = self.track_step( + frame_idx=frame_idx, + is_init_cond_frame=is_init_cond_frame, + backbone_features_interactive=backbone_features_interactive, + backbone_features_propagation=backbone_features_propagation, + image=image, + point_inputs=point_inputs, + mask_inputs=mask_inputs, + gt_masks=None, + frames_to_add_correction_pt=[], + output_dict=output_dict, + num_frames=inference_state["num_frames"], + track_in_reverse=reverse, + run_mem_encoder=run_mem_encoder, + prev_sam_mask_logits=prev_sam_mask_logits, + multiplex_state=inference_state["multiplex_state"], + objects_to_interact=objects_to_interact, + ) + + # optionally offload the output to CPU memory to save GPU space + storage_device = inference_state["storage_device"] + if current_out.get("maskmem_features") is not None: + maskmem_features = current_out["maskmem_features"] + maskmem_features = maskmem_features.to( + device=storage_device, dtype=torch.bfloat16, non_blocking=True + ) + else: + maskmem_features = None + + if current_out.get("image_features") is not None: + assert "image_pos_enc" in current_out + image_features = current_out["image_features"].to( + storage_device, non_blocking=True + ) + image_pos_enc = current_out["image_pos_enc"].to( + storage_device, non_blocking=True + ) + else: + image_features = image_pos_enc = None + + pred_masks_gpu = current_out["pred_masks"] + pred_masks = pred_masks_gpu.to(storage_device, non_blocking=True) + # "maskmem_pos_enc" is the same across frames, so we only need to store one copy of it + with torch.profiler.record_function( + "VideoTrackingMultiplexDemo.maskmem_pos_enc" + ): + maskmem_pos_enc = self._get_maskmem_pos_enc(inference_state, current_out) + # object pointer is a small tensor, so we always keep it on GPU memory for fast access + obj_ptr = current_out["obj_ptr"] + object_score_logits = current_out["object_score_logits"] + conditioning_objects = current_out["conditioning_objects"] + # make a compact version of this frame's output to reduce the state size + compact_current_out = { + "maskmem_features": maskmem_features, + "maskmem_pos_enc": maskmem_pos_enc, + "image_features": image_features, + "image_pos_enc": image_pos_enc, + "pred_masks": pred_masks, + "obj_ptr": obj_ptr, + "object_score_logits": object_score_logits, + "conditioning_objects": conditioning_objects, + } + if self.use_memory_selection: + with torch.profiler.record_function( + "VideoTrackingMultiplexDemo.use_memory_selection" + ): + compact_current_out["iou_score"] = current_out["iou_score"] + compact_current_out["eff_iou_score"] = self.cal_mem_score( + object_score_logits, current_out["iou_score"] + ) + return compact_current_out, pred_masks_gpu + + def _run_memory_encoder( + self, + inference_state, + frame_idx, + batch_size, + high_res_masks, + object_score_logits, + is_mask_from_pts, + conditioning_objects=None, # Accept as parameter + ): + """ + Run the memory encoder on `high_res_masks`. This is usually after applying + non-overlapping constraints to object scores. Since their scores changed, their + memory also need to be computed again with the memory encoder. + """ + # Retrieve correct image features + image, backbone_features = self._get_image_feature( + inference_state, frame_idx, batch_size + ) + backbone_features_propagation = backbone_features["sam2_backbone_out"] + propagation_vision_feats = backbone_features_propagation["vision_feats"] + propagation_vision_pos_embeds = backbone_features_propagation[ + "vision_pos_embeds" + ] + propagation_feat_sizes = backbone_features_propagation["feat_sizes"] + + # If conditioning_objects is not provided, look it up from output_dict + if conditioning_objects is None: + output_dict = inference_state["output_dict"] + for storage_key in ["cond_frame_outputs", "non_cond_frame_outputs"]: + storage = output_dict[storage_key] + if frame_idx not in storage: + continue + conditioning_objects = storage[frame_idx]["conditioning_objects"] + break + else: + raise ValueError(f"conditioning objects not found at {frame_idx=}") + + maskmem_features, maskmem_pos_enc = self._encode_new_memory( + image=image, + current_vision_feats=propagation_vision_feats, + feat_sizes=propagation_feat_sizes, + pred_masks_high_res=high_res_masks, + object_score_logits=object_score_logits, + is_mask_from_pts=is_mask_from_pts, + conditioning_objects=conditioning_objects, + multiplex_state=inference_state["multiplex_state"], + ) + + # optionally offload the output to CPU memory to save GPU space + storage_device = inference_state["storage_device"] + maskmem_features = maskmem_features.to(torch.bfloat16) + maskmem_features = maskmem_features.to(storage_device, non_blocking=True) + # "maskmem_pos_enc" is the same across frames, so we only need to store one copy of it + maskmem_pos_enc = self._get_maskmem_pos_enc( + inference_state, {"maskmem_pos_enc": maskmem_pos_enc} + ) + + image_features = propagation_vision_feats[-1] + image_features = image_features.to(storage_device, non_blocking=True) + image_pos_enc = propagation_vision_pos_embeds[-1] + image_pos_enc = image_pos_enc.to(storage_device, non_blocking=True) + return maskmem_features, maskmem_pos_enc, image_features, image_pos_enc + + def _get_maskmem_pos_enc(self, inference_state, current_out): + """ + `maskmem_pos_enc` is the same across frames and objects, so we cache it as + a constant in the inference session to reduce session storage size. + """ + model_constants = inference_state["constants"] + # "out_maskmem_pos_enc" should be either a list of tensors or None + out_maskmem_pos_enc = current_out.get("maskmem_pos_enc") + if out_maskmem_pos_enc is not None: + if "maskmem_pos_enc" not in model_constants: + assert isinstance(out_maskmem_pos_enc, list) + # only take the slice for one object, since it's same across objects + maskmem_pos_enc = [x[0:1].clone() for x in out_maskmem_pos_enc] + model_constants["maskmem_pos_enc"] = maskmem_pos_enc + else: + maskmem_pos_enc = model_constants["maskmem_pos_enc"] + # expand the cached maskmem_pos_enc to the actual batch size + batch_size = out_maskmem_pos_enc[0].size(0) + expanded_maskmem_pos_enc = [ + x.expand(batch_size, -1, -1, -1) for x in maskmem_pos_enc + ] + else: + expanded_maskmem_pos_enc = None + return expanded_maskmem_pos_enc + + @torch.inference_mode() + def remove_object( + self, + inference_state, + obj_id: int, + strict=False, + need_output=True, + clear_user_refined_map: bool = True, + ): + """ + Remove a single object from the tracking state. + + This is a convenience wrapper around remove_objects() for removing a single object. + + Args: + inference_state: Current inference state + obj_id: Object ID to remove + strict: If True, raise error if object doesn't exist + need_output: Whether to return updated frames + + Returns: + Tuple of (remaining_obj_ids, updated_frames) + """ + return self.remove_objects( + inference_state, + obj_ids=[obj_id], + strict=strict, + need_output=need_output, + clear_user_refined_map=clear_user_refined_map, + ) + + @torch.inference_mode() + def remove_objects( + self, + inference_state, + obj_ids: Iterable[int], + strict=False, + need_output=True, + clear_user_refined_map: bool = True, + ): + """ + Remove a list of object ids from the tracking state. If strict is True, we check whether + the object ids actually exist and raise an error if any of them don't exist. + """ + obj_ids = list(obj_ids) + old_obj_idxs_to_rm = [ + inference_state["obj_id_to_idx"].get(obj_id, None) for obj_id in obj_ids + ] + updated_frames = [] + actually_used_obj_ids = [] + removing_any = False + for old_obj_idx_to_rm, obj_id in zip(old_obj_idxs_to_rm, obj_ids, strict=True): + if old_obj_idx_to_rm is None: + if strict: + raise ValueError( + f"Object id {obj_id} does not exist in the tracking state." + ) + else: + actually_used_obj_ids.append(obj_id) + removing_any = True + if not removing_any: + return inference_state["obj_ids"], updated_frames + + # ignore any object IDs that don't exist + old_obj_idxs_to_rm = [x for x in old_obj_idxs_to_rm if x is not None] + obj_ids = actually_used_obj_ids + removed_obj_ids = list(obj_ids) + + # There are still remaining objects after removing this object id. In this case, + # we need to delete the object storage from inference state tensors. + # Step 0: clear the input on those frames where this object id has point or mask input + # (note that this step is required as it might downgrade conditioning frames to + # non-conditioning ones) + if clear_user_refined_map and "user_refined_frames_per_obj" in inference_state: + user_refined_map = inference_state["user_refined_frames_per_obj"] + for removed_obj_id in removed_obj_ids: + if removed_obj_id in user_refined_map: + user_refined_map.pop(removed_obj_id, None) + + all_obj_input_frames_inds = set() + for old_obj_idx_to_rm, obj_id in zip(old_obj_idxs_to_rm, obj_ids, strict=True): + obj_input_frames_inds = set() + obj_input_frames_inds.update( + inference_state["point_inputs_per_obj"][old_obj_idx_to_rm] + ) + obj_input_frames_inds.update( + inference_state["mask_inputs_per_obj"][old_obj_idx_to_rm] + ) + for frame_idx in obj_input_frames_inds: + self.clear_all_points_in_frame( + inference_state, + frame_idx, + obj_id, + need_output=False, + preserve_user_refined=not clear_user_refined_map, + ) + all_obj_input_frames_inds.update(obj_input_frames_inds) + + # Step 1: Update the object id mapping (note that it must be done after Step 0, + # since Step 0 still requires the old object id mappings in inference_state) + old_obj_ids = inference_state["obj_ids"] + old_obj_inds = list(range(len(old_obj_ids))) + remain_old_obj_inds = old_obj_inds.copy() + for old_obj_idx_to_rm in old_obj_idxs_to_rm: + remain_old_obj_inds.remove(old_obj_idx_to_rm) + new_obj_ids = [old_obj_ids[old_idx] for old_idx in remain_old_obj_inds] + new_obj_inds = list(range(len(new_obj_ids))) + # build new mappings + old_idx_to_new_idx = dict(zip(remain_old_obj_inds, new_obj_inds)) + inference_state["obj_id_to_idx"] = dict(zip(new_obj_ids, new_obj_inds)) + inference_state["obj_idx_to_id"] = dict(zip(new_obj_inds, new_obj_ids)) + inference_state["obj_ids"] = new_obj_ids + + if len(new_obj_ids) == 0: + return new_obj_ids, updated_frames + + # Step 2: For per-object tensor storage, we shift their obj_idx in the dict keys. + # (note that "consolidated_frame_inds" doesn't need to be updated in this step as + # it's already handled in Step 0) + def _map_keys(container): + new_kvs = [] + for k in old_obj_inds: + v = container.pop(k) + if k in old_idx_to_new_idx: + new_kvs.append((old_idx_to_new_idx[k], v)) + container.update(new_kvs) + + _map_keys(inference_state["point_inputs_per_obj"]) + _map_keys(inference_state["mask_inputs_per_obj"]) + _map_keys(inference_state["output_dict_per_obj"]) + _map_keys(inference_state["temp_output_dict_per_obj"]) + + multiplex_state: MultiplexState = inference_state["multiplex_state"] + # strict is set to True because we have done the filtering above + buckets_to_keep = multiplex_state.remove_objects( + old_obj_idxs_to_rm, strict=True + ) + obj_ids = set(obj_ids) + + # Step 3: For packed tensor storage, we index the remaining ids and rebuild the per-bucket/per-object slices. + def _slice_state(output_dict, storage_key): + for frame_idx, out in output_dict[storage_key].items(): + out["maskmem_features"] = out["maskmem_features"][buckets_to_keep] + out["maskmem_pos_enc"] = [ + x[buckets_to_keep] for x in out["maskmem_pos_enc"] + ] + # "maskmem_pos_enc" is the same across frames, so we only need to store one copy of it + out["maskmem_pos_enc"] = self._get_maskmem_pos_enc(inference_state, out) + out["obj_ptr"] = out["obj_ptr"][buckets_to_keep] + + # Note that pred_maks and score_logits are stored in a per-object manner + # When we add new objects, obj_id_to_idx mapping could be different + # locally (at this past frame) versus globally (at the current frame), + # so we need to use a local copy of this mapping + local_obj_id_to_idx = out["local_obj_id_to_idx"] + + # Find which local indices correspond to the remaining old object indices + local_remain_old_obj_inds = [ + obj_idx + for obj_id, obj_idx in local_obj_id_to_idx.items() + if obj_id not in obj_ids + ] + + # Guard against stale indices by intersecting with available rows + max_pred = out["pred_masks"].shape[0] + max_scores = out["object_score_logits"].shape[0] + keep_indices = [ + idx + for idx in local_remain_old_obj_inds + if 0 <= idx < max_pred and 0 <= idx < max_scores + ] + out["pred_masks"] = out["pred_masks"][keep_indices] + out["object_score_logits"] = out["object_score_logits"][keep_indices] + if self.use_memory_selection: + out["iou_score"] = out["iou_score"][keep_indices] + out["eff_iou_score"] = self.cal_mem_score( + out["object_score_logits"], out["iou_score"] + ) # recalculate the memory frame score + sliced_conditioning_objects = set() + + # Update local_obj_id_to_idx to reflect the new indices after removal + new_local_obj_id_to_idx = {} + old_to_new = { + old_idx: new_i for new_i, old_idx in enumerate(keep_indices) + } + for obj_id, old_idx in local_obj_id_to_idx.items(): + if obj_id not in obj_ids: # Keep objects not being removed + # Find the new index for this object if it was kept + if old_idx in old_to_new: + new_idx = old_to_new[old_idx] + new_local_obj_id_to_idx[obj_id] = new_idx + if old_idx in out["conditioning_objects"]: + sliced_conditioning_objects.add(new_idx) + + out["local_obj_id_to_idx"] = new_local_obj_id_to_idx + out["conditioning_objects"] = sliced_conditioning_objects + + # also update the per-object slices + self._add_output_per_object( + inference_state, frame_idx, out, storage_key + ) + + _slice_state(inference_state["output_dict"], "cond_frame_outputs") + _slice_state(inference_state["output_dict"], "non_cond_frame_outputs") + + # Step 4: Further collect the outputs on those frames in `obj_input_frames_inds`, which + # could show an updated mask for objects previously occluded by the object being removed + if need_output: + temp_output_dict_per_obj = inference_state["temp_output_dict_per_obj"] + for frame_idx in all_obj_input_frames_inds: + is_cond = any( + frame_idx in obj_temp_output_dict["cond_frame_outputs"] + for obj_temp_output_dict in temp_output_dict_per_obj.values() + ) + consolidated_out = self._consolidate_temp_output_across_obj( + inference_state, + frame_idx, + is_cond=is_cond, + run_mem_encoder=False, + consolidate_at_video_res=True, + ) + _, video_res_masks = self._get_orig_video_res_output( + inference_state, consolidated_out["pred_masks_video_res"] + ) + updated_frames.append((frame_idx, video_res_masks)) + + return inference_state["obj_ids"], updated_frames + + def _clear_non_cond_mem_around_input(self, inference_state, frame_idx): + """ + Remove the non-conditioning memory around the input frame. When users provide + correction clicks, the surrounding frames' non-conditioning memories can still + contain outdated object appearance information and could confuse the model. + + This function clears those non-conditioning memories surrounding the interacted + frame to avoid giving the model both old and new information about the object. + """ + r = self.memory_temporal_stride_for_eval + frame_idx_begin = frame_idx - r * self.num_maskmem + frame_idx_end = frame_idx + r * self.num_maskmem + output_dict = inference_state["output_dict"] + non_cond_frame_outputs = output_dict["non_cond_frame_outputs"] + for t in range(frame_idx_begin, frame_idx_end + 1): + non_cond_frame_outputs.pop(t, None) + for obj_output_dict in inference_state["output_dict_per_obj"].values(): + obj_output_dict["non_cond_frame_outputs"].pop(t, None) + + @torch.inference_mode() + @torch.autocast(device_type="cuda", dtype=torch.bfloat16) + def warm_up_compilation( + self, offload_video_to_cpu=False, offload_state_to_cpu=False + ): + """ + Warm up the model by running a dummy inference to compile the model. This is + useful to avoid the compilation overhead in the first inference call. + """ + if not self.compile_all_components: + return + + raise NotImplementedError( + "Please use `VideoTrackingMultiplexDemoPerBucketInference` instead for full model compilation." + ) + + +class Sam3VideoTrackingMultiplexDemo(VideoTrackingMultiplexDemo): + @torch.inference_mode() + def init_state( + self, + video_height, + video_width, + num_frames, + cached_features=None, + offload_video_to_cpu=False, + offload_state_to_cpu=False, + ): + """Initialize a inference state.""" + # Make sure that sigmoid is used on mask logits (should be True for all our recent models). + # Since we rely on large negative values as scores for missing objects, the raw logits + # cannot be consumed directly and must be converted into 0~1 range via sigmoid first. + if not self.apply_sigmoid_to_mask_logits_for_mem_enc: + raise NotImplementedError( + "Multi-object tracking requires sigmoid in memory encoder for non-overlapping constraints." + ) + inference_state = {} + # inference_state["images"] = images + inference_state["num_frames"] = num_frames + # whether to offload the video frames to CPU memory + # turning on this option saves the GPU memory with only a very small overhead + inference_state["offload_video_to_cpu"] = offload_video_to_cpu + # whether to offload the inference state to CPU memory + # turning on this option saves the GPU memory at the cost of a lower tracking fps + # (e.g. in a test case of 768x768 model, fps dropped from 27 to 24 when tracking one object + # and from 24 to 21 when tracking two objects) + inference_state["offload_state_to_cpu"] = offload_state_to_cpu + # the original video height and width, used for resizing final output scores + inference_state["video_height"] = video_height + inference_state["video_width"] = video_width + inference_state["device"] = torch.device("cuda") + if offload_state_to_cpu: + inference_state["storage_device"] = torch.device("cpu") + else: + inference_state["storage_device"] = torch.device("cuda") + # inputs on each frame + inference_state["point_inputs_per_obj"] = {} + inference_state["mask_inputs_per_obj"] = {} + # visual features on a small number of recently visited frames for quick interactions + inference_state["cached_features"] = ( + {} if cached_features is None else cached_features + ) + # values that don't change across frames (so we only need to hold one copy of them) + inference_state["constants"] = {} + # mapping between client-side object id and model-side object index + inference_state["obj_id_to_idx"] = OrderedDict() + inference_state["obj_idx_to_id"] = OrderedDict() + inference_state["obj_ids"] = [] + # A storage to hold the model's tracking results and states on each frame + inference_state["output_dict"] = { + "cond_frame_outputs": {}, # dict containing {frame_idx: } + "non_cond_frame_outputs": {}, # dict containing {frame_idx: } + } + # The index of the frame that received the first annotation + inference_state["first_ann_frame_idx"] = None + # Slice (view) of each object tracking results, sharing the same memory with "output_dict" + inference_state["output_dict_per_obj"] = {} + # A temporary storage to hold new outputs when user interact with a frame + # to add clicks or mask (it's merged into "output_dict" before propagation starts) + inference_state["temp_output_dict_per_obj"] = {} + # Frames that already holds consolidated outputs from click or mask inputs + # (we directly use their consolidated outputs during tracking) + inference_state["consolidated_frame_inds"] = { + "cond_frame_outputs": set(), # set containing frame indices + "non_cond_frame_outputs": set(), # set containing frame indices + } + # metadata for each tracking frame (e.g. which direction it's tracked) + inference_state["tracking_has_started"] = False + inference_state["frames_already_tracked"] = {} + inference_state["multiplex_state"] = None + # Warm up the whole model and cache the image feature on frame 0 + # by making a dummy click on the first frame (and then cleaning it up) + # self.add_new_points( + # inference_state=inference_state, + # frame_idx=0, + # obj_id=1, + # points=torch.tensor([[0.5, 0.5]], dtype=torch.float32), + # labels=torch.tensor([1], dtype=torch.int32), + # clear_old_points=True, + # rel_coordinates=True, + # ) + self.clear_all_points_in_video(inference_state) + return inference_state + + def _suppress_shrinked_masks( + self, pred_masks, new_pred_masks, shrink_threshold=0.3 + ): + area_before = (pred_masks > 0).sum(dim=(-1, -2)) + area_after = (new_pred_masks > 0).sum(dim=(-1, -2)) + area_before = torch.clamp(area_before, min=1.0) + area_ratio = area_after / area_before + keep = area_ratio >= shrink_threshold + keep_mask = keep[..., None, None].expand_as(pred_masks) + pred_masks_after = torch.where( + keep_mask, pred_masks, torch.clamp(pred_masks, max=-10.0) + ) + return pred_masks_after + + @staticmethod + def _suppress_object_pw_area_shrinkage(pred_masks): + """ + This function suppresses masks that shrink in area after applying pixelwise non-overlapping constriants. + Note that the final output can still be overlapping. + """ + # Apply pixel-wise non-overlapping constraint based on mask scores + # pixel_level_non_overlapping_masks = super()._apply_non_overlapping_constraints( + # pred_masks + # ) + + batch_size = pred_masks.size(0) + if batch_size == 1: + return pred_masks + + device = pred_masks.device + # "max_obj_inds": object index of the object with the highest score at each location + max_obj_inds = torch.argmax(pred_masks, dim=0, keepdim=True) + # "batch_obj_inds": object index of each object slice (along dim 0) in `pred_masks` + batch_obj_inds = torch.arange(batch_size, device=device)[:, None, None, None] + keep = max_obj_inds == batch_obj_inds + # suppress overlapping regions' scores below -10.0 so that the foreground regions + # don't overlap (here sigmoid(-10.0)=4.5398e-05) + pixel_level_non_overlapping_masks = torch.where( + keep, pred_masks, torch.clamp(pred_masks, max=-10.0) + ) + + # Fully suppress masks with high shrinkage (probably noisy) based on the pixel wise non-overlapping constraints + # NOTE: The output of this function can be a no op if none of the masks shrinked by a large factor. + # pred_masks = self._suppress_shrinked_masks( + # pred_masks, pixel_level_non_overlapping_masks + # ) + + shrink_threshold = 0.3 + area_before = (pred_masks > 0).sum(dim=(-1, -2)) + area_after = (pixel_level_non_overlapping_masks > 0).sum(dim=(-1, -2)) + area_before = torch.clamp(area_before, min=1.0) + area_ratio = area_after / area_before + keep = area_ratio >= shrink_threshold + keep_mask = keep[..., None, None].expand_as(pred_masks) + pred_masks_after = torch.where( + keep_mask, pred_masks, torch.clamp(pred_masks, max=-10.0) + ) + + return pred_masks_after + + def _apply_object_wise_non_overlapping_constraints( + self, pred_masks, obj_scores, background_value=-10.0 + ): + """ + Applies non-overlapping constraints object wise (i.e. only one object can claim the overlapping region) + """ + # TODO: Try suppression based on IoM here as well. + # Replace pixel scores with object scores + pred_masks_single_score = torch.where( + pred_masks > 0, obj_scores[..., None, None], background_value + ) + # Apply pixel-wise non-overlapping constraint based on mask scores + pixel_level_non_overlapping_masks = super()._apply_non_overlapping_constraints( + pred_masks_single_score + ) + # Replace object scores with pixel scores. Note, that now only one object can claim the overlapping region + pred_masks = torch.where( + pixel_level_non_overlapping_masks > 0, + pred_masks, + torch.clamp(pred_masks, max=background_value), + ) + return pred_masks + + @torch.inference_mode() + def propagate_in_video( + self, + inference_state, + start_frame_idx, + max_frame_num_to_track, + reverse, + tqdm_disable=False, + obj_ids=None, + run_mem_encoder=True, + ): + """Propagate the input points across frames to track in the entire video.""" + # NOTE: This is a copy from the parent class, except that we return object scores as well. + output_dict = inference_state["output_dict"] + consolidated_frame_inds = inference_state["consolidated_frame_inds"] + if obj_ids is not None: + raise NotImplementedError( + "Per-object tracking yet for batched inference if not implemented." + ) + obj_ids = inference_state["obj_ids"] + batch_size = self._get_obj_num(inference_state) + if len(output_dict["cond_frame_outputs"]) == 0: + raise RuntimeError("No points are provided; please add points first") + clear_non_cond_mem = self.clear_non_cond_mem_around_input and ( + self.clear_non_cond_mem_for_multi_obj or batch_size <= 1 + ) + + processing_order = self._get_processing_order( + inference_state, + start_frame_idx, + max_frame_num_to_track, + reverse, + ) + + for frame_idx in tqdm( + processing_order, desc="propagate in video", disable=tqdm_disable + ): + # We skip those frames already in consolidated outputs (these are frames + # that received input clicks or mask). Note that we cannot directly run + # batched forward on them via `_run_single_frame_inference` because the + # number of clicks on each object might be different. + if frame_idx in consolidated_frame_inds["cond_frame_outputs"]: + storage_key = "cond_frame_outputs" + current_out = output_dict[storage_key][frame_idx] + pred_masks = current_out["pred_masks"] + obj_scores = current_out["object_score_logits"] + if clear_non_cond_mem: + # clear non-conditioning memory of the surrounding frames + self._clear_non_cond_mem_around_input(inference_state, frame_idx) + elif frame_idx in consolidated_frame_inds["non_cond_frame_outputs"]: + storage_key = "non_cond_frame_outputs" + current_out = output_dict[storage_key][frame_idx] + pred_masks = current_out["pred_masks"] + obj_scores = current_out["object_score_logits"] + else: + storage_key = "non_cond_frame_outputs" + with torch.profiler.record_function( + "VideoTrackingMultiplexDemo._run_single_frame_inference" + ): + current_out, pred_masks = self._run_single_frame_inference( + inference_state=inference_state, + output_dict=output_dict, + frame_idx=frame_idx, + batch_size=batch_size, + is_init_cond_frame=False, + point_inputs=None, + mask_inputs=None, + reverse=reverse, + run_mem_encoder=run_mem_encoder, + ) + obj_scores = current_out["object_score_logits"] + current_out["local_obj_id_to_idx"] = deepcopy( + inference_state["obj_id_to_idx"] + ) + output_dict[storage_key][frame_idx] = current_out + + # Create slices of per-object outputs for subsequent interaction with each + # individual object after tracking. + self._add_output_per_object( + inference_state, frame_idx, current_out, storage_key + ) + inference_state["frames_already_tracked"][frame_idx] = {"reverse": reverse} + + # Resize the output mask to the original video resolution (we directly use + # the mask scores on GPU for output to avoid any CPU conversion in between) + low_res_masks, video_res_masks = self._get_orig_video_res_output( + inference_state, pred_masks + ) + yield frame_idx, obj_ids, low_res_masks, video_res_masks, obj_scores diff --git a/sam3/model/vitdet.py b/sam3/model/vitdet.py index bc4eeb0..90d42d7 100644 --- a/sam3/model/vitdet.py +++ b/sam3/model/vitdet.py @@ -22,13 +22,58 @@ import torch.utils.checkpoint as checkpoint try: - from timm.layers import DropPath, Mlp, trunc_normal_ + from timm.layers import DropPath, trunc_normal_ except ModuleNotFoundError: # compatibility for older timm versions - from timm.models.layers import DropPath, Mlp, trunc_normal_ + from timm.models.layers import DropPath, trunc_normal_ +from sam3.model.data_misc import NestedTensor +from sam3.model.model_misc import AttentionType, LayerScale +from sam3.perflib.fused import addmm_act +from sam3.sam.rope import apply_rotary_enc_real, VisionRotaryEmbeddingVE from torch import Tensor -from .model_misc import LayerScale + +class Mlp(nn.Module): + """MLP as used in Vision Transformer, MLP-Mixer and related networks""" + + def __init__( + self, + in_features, + hidden_features=None, + out_features=None, + act_layer=nn.GELU, + norm_layer=None, + bias=True, + drop=0.0, + use_conv=False, + ): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + if isinstance(bias, bool): + bias = (bias, bias) + if isinstance(drop, (int, float)): + drop_probs = (drop, drop) + else: + drop_probs = drop + linear_layer = partial(nn.Conv2d, kernel_size=1) if use_conv else nn.Linear + + self.fc1 = linear_layer(in_features, hidden_features, bias=bias[0]) + self.act = act_layer() + self.drop1 = nn.Dropout(drop_probs[0]) + self.norm = ( + norm_layer(hidden_features) if norm_layer is not None else nn.Identity() + ) + self.fc2 = linear_layer(hidden_features, out_features, bias=bias[1]) + self.drop2 = nn.Dropout(drop_probs[1]) + + def forward(self, x): + x = addmm_act(type(self.act), self.fc1, x) + x = self.drop1(x) + x = self.norm(x) + x = self.fc2(x) + x = self.drop2(x) + return x def init_t_xy( @@ -349,11 +394,16 @@ def __init__( use_rel_pos: bool = False, rel_pos_zero_init: bool = True, input_size: Optional[Tuple[int, int]] = None, + attn_type: AttentionType = AttentionType.Vanilla, cls_token: bool = False, use_rope: bool = False, rope_theta: float = 10000.0, rope_pt_size: Optional[Tuple[int, int]] = None, rope_interp: bool = False, + rope_tiled: bool = False, + use_ve_rope: bool = False, + use_fa3: bool = False, + use_rope_real: bool = False, ): """ Args: @@ -369,7 +419,9 @@ def __init__( use_rope: whether to use rope 2d (indep of use_rel_pos, as it can be used together) rope_theta: control frequencies of rope rope_pt_size: size of rope in previous stage of training, needed for interpolation or tiling + rope_tiled: whether to tile rope or not; tile expected to be of size rope_pt_size x rope_pt_size rope_interp: whether to interpolate (or extrapolate) rope to match input size + use_ve_rope: use ve orig rope implementation, if small numerical differences are important (normally not) """ super().__init__() self.num_heads = num_heads @@ -377,6 +429,7 @@ def __init__( self.scale = self.head_dim**-0.5 self.cls_token = cls_token + self.attn_type = attn_type self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) self.proj = nn.Linear(dim, dim) @@ -388,6 +441,10 @@ def __init__( self.rope_theta = rope_theta self.rope_pt_size = rope_pt_size self.rope_interp = rope_interp + self.rope_tiled = rope_tiled + self.use_ve_rope = use_ve_rope + self.use_fa3 = use_fa3 + self.use_rope_real = use_rope_real # init rel_pos embeddings and rope self._setup_rel_pos(rel_pos_zero_init) @@ -430,6 +487,15 @@ def _setup_rope_freqs(self) -> None: if self.rope_pt_size is None: self.rope_pt_size = self.input_size + if self.use_ve_rope: + assert not self.rope_tiled, "not supported" + self.rope = VisionRotaryEmbeddingVE( + dim=self.head_dim // 2, + seq_len=self.input_size[0], + pt_seq_len=self.rope_pt_size[0], + ) + return + # initialize 2d rope freqs self.compute_cis = partial( compute_axial_cis, @@ -437,16 +503,40 @@ def _setup_rope_freqs(self) -> None: theta=self.rope_theta, ) - # interpolate rope - scale_pos = 1.0 - if self.rope_interp: - scale_pos = self.rope_pt_size[0] / self.input_size[0] - # get scaled freqs_cis - freqs_cis = self.compute_cis( - end_x=self.input_size[0], - end_y=self.input_size[1], - scale_pos=scale_pos, - ) + if self.rope_pt_size != self.input_size and self.rope_tiled: + assert not self.rope_interp + # window/tiled rope + freqs_cis = self.compute_cis( + end_x=self.rope_pt_size[0], end_y=self.rope_pt_size[1] + ) + # check dims are tileable + rh, rw = ( + self.input_size[0] // self.rope_pt_size[0], + self.input_size[1] // self.rope_pt_size[1], + ) + assert rh >= 1, rw >= 1 + assert ( + self.input_size[0] % self.rope_pt_size[0] == 0 + and self.input_size[1] % self.rope_pt_size[1] == 0 + ) + + # restore spatial shape, tile and then flatten spatial dims + freqs_cis = ( + freqs_cis.reshape(self.rope_pt_size[0], self.rope_pt_size[1], -1) + .tile(rh, rw, 1) + .reshape(-1, freqs_cis.shape[-1]) + ) + else: + # interpolate rope + scale_pos = 1.0 + if self.rope_interp: + scale_pos = self.rope_pt_size[0] / self.input_size[0] + # get scaled freqs_cis + freqs_cis = self.compute_cis( + end_x=self.input_size[0], + end_y=self.input_size[1], + scale_pos=scale_pos, + ) if self.cls_token: t = torch.zeros( self.head_dim // 2, @@ -457,12 +547,27 @@ def _setup_rope_freqs(self) -> None: freqs_cis = torch.cat([cls_freqs_cis, freqs_cis], dim=0) self.register_buffer("freqs_cis", freqs_cis) + if self.use_rope_real: + self.register_buffer("freqs_cis_real", freqs_cis.real) + self.register_buffer("freqs_cis_imag", freqs_cis.imag) def _apply_rope(self, q, k) -> Tuple[Tensor, Tensor]: if not self.use_rope: return q, k + if self.use_ve_rope: + dtype = q.dtype + return self.rope(q).to(dtype), self.rope(k).to(dtype) + assert self.freqs_cis is not None + + if self.use_rope_real: + return apply_rotary_enc_real( + q, + k, + freqs_cis_imag=self.freqs_cis_imag, + freqs_cis_real=self.freqs_cis_real, + ) return apply_rotary_enc(q, k, freqs_cis=self.freqs_cis) def forward(self, x: Tensor) -> Tensor: @@ -501,7 +606,17 @@ def forward(self, x: Tensor) -> Tensor: q = q.reshape(B, self.num_heads, H * W, -1) k = k.reshape(B, self.num_heads, H * W, -1) - x = F.scaled_dot_product_attention(q, k, v) + if self.attn_type == AttentionType.Vanilla: + if self.use_fa3: + from sam3.perflib.fa3 import flash_attn_func + + x = flash_attn_func( + q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2) + ).transpose(1, 2) + else: + x = F.scaled_dot_product_attention(q, k, v) + else: + raise NotImplementedError if ndim == 4: x = ( @@ -541,6 +656,9 @@ def __init__( cls_token: bool = False, dropout: float = 0.0, init_values: Optional[float] = None, + attn_type: AttentionType = AttentionType.Vanilla, + use_fa3: bool = False, + use_rope_real: bool = False, ): """ Args: @@ -561,8 +679,10 @@ def __init__( cls_token: whether a cls_token is present. use_rope: whether to use rope 2d (indep of use_rel_pos, as it can be used together) rope_pt_size: size of rope in previous stage of training, needed for interpolation or tiling + rope_tiled: whether to tile rope or not; tile expected to be of size rope_pt_size x rope_pt_size rope_interp: whether to interpolate (or extrapolate) rope to match target input size, expected to specify source size as rope_pt_size. + use_ve_rope: use ve orig rope implementation, if small numerical differences are important (normally not) """ super().__init__() self.norm1 = norm_layer(dim) @@ -573,10 +693,15 @@ def __init__( use_rel_pos=use_rel_pos, rel_pos_zero_init=rel_pos_zero_init, input_size=input_size if window_size == 0 else (window_size, window_size), + attn_type=attn_type, use_rope=use_rope, rope_pt_size=rope_pt_size, + rope_tiled=rope_tiled, rope_interp=rope_interp, + use_ve_rope=use_ve_rope, cls_token=cls_token, + use_fa3=use_fa3, + use_rope_real=use_rope_real, ) self.ls1 = ( LayerScale(dim, init_values=init_values) if init_values else nn.Identity() @@ -642,19 +767,24 @@ def __init__( window_size: int = 14, global_att_blocks: Tuple[int, ...] = (2, 5, 8, 11), use_rope: bool = False, + use_tiled_rope: bool = False, rope_pt_size: Optional[int] = None, use_interp_rope: bool = False, + use_ve_rope: bool = False, + use_act_checkpoint: bool = True, pretrain_img_size: int = 224, pretrain_use_cls_token: bool = True, retain_cls_token: bool = True, dropout: float = 0.0, return_interm_layers: bool = False, init_values: Optional[float] = None, # for layerscale + attn_type: AttentionType = AttentionType.Vanilla, ln_pre: bool = False, ln_post: bool = False, bias_patch_embed: bool = True, compile_mode: Optional[str] = None, - use_act_checkpoint: bool = True, + use_fa3: bool = False, + use_rope_real: bool = False, ): """ Args: @@ -708,9 +838,9 @@ def __init__( self.retain_cls_token = retain_cls_token if self.retain_cls_token: assert pretrain_use_cls_token - assert len(window_block_indexes) == 0, ( - "windowing not supported with cls token" - ) + assert ( + len(window_block_indexes) == 0 + ), "windowing not supported with cls token" assert sum(self.rel_pos_blocks) == 0, "rel pos not supported with cls token" @@ -768,10 +898,15 @@ def __init__( if rope_pt_size is None else (rope_pt_size, rope_pt_size) ), + rope_tiled=use_tiled_rope, + use_ve_rope=use_ve_rope, rope_interp=use_interp_rope, cls_token=self.retain_cls_token, dropout=dropout, init_values=init_values, + attn_type=attn_type, + use_fa3=use_fa3, + use_rope_real=use_rope_real, ) if i not in window_block_indexes: @@ -812,7 +947,14 @@ def _init_weights(self, m: nn.Module) -> None: nn.init.constant_(m.bias, 0) nn.init.constant_(m.weight, 1.0) - def forward(self, x: torch.Tensor) -> List[torch.Tensor]: + def forward(self, tensor_list): + if isinstance(tensor_list, NestedTensor): + x = tensor_list.tensors + mask = tensor_list.mask + else: + x = tensor_list + mask = None + x = self.patch_embed(x) h, w = x.shape[1], x.shape[2] @@ -835,6 +977,7 @@ def forward(self, x: torch.Tensor) -> List[torch.Tensor]: x = self.ln_pre(x) outputs = [] + masks = None for i, blk in enumerate(self.blocks): if self.use_act_checkpoint and self.training: x = checkpoint.checkpoint(blk, x, use_reentrant=False) @@ -856,7 +999,15 @@ def forward(self, x: torch.Tensor) -> List[torch.Tensor]: feats.shape[0], h, w, feats.shape[-1] ).permute(0, 3, 1, 2) - outputs.append(feats) + if isinstance(tensor_list, NestedTensor): + # Optimization, if the mask is all False, just ignore it + if mask is not None and mask.any() and masks is None: + masks = F.interpolate( + mask[None].float(), size=feats.shape[-2:] + ).bool()[0] + outputs.append(NestedTensor(feats, masks)) + else: + outputs.append(feats) return outputs diff --git a/sam3/model/vl_combiner.py b/sam3/model/vl_combiner.py index faf5504..aed1f2c 100644 --- a/sam3/model/vl_combiner.py +++ b/sam3/model/vl_combiner.py @@ -12,7 +12,8 @@ from torch.nn.attention import sdpa_kernel, SDPBackend from .act_ckpt_utils import activation_ckpt_wrapper -from .necks import Sam3DualViTDetNeck +from .data_misc import NestedTensor +from .necks import Sam3DualViTDetNeck, Sam3TriViTDetNeck class SAM3VLBackbone(nn.Module): @@ -175,3 +176,255 @@ def _forward_text_no_ack_ckpt( ) return output + + +class SAM3VLBackboneTri(SAM3VLBackbone): + """VL backbone with triple-head vision (sam3, interactive, propagation) + text encoder.""" + + def __init__(self, visual, text, compile_visual=False, scalp=0): + super().__init__( + visual=visual, text=text, compile_visual=compile_visual, scalp=scalp + ) + assert isinstance( + self.vision_backbone, Sam3TriViTDetNeck + ), f"Expected vision backbone to be of type Sam3TriViTDetNeck, got {type(self.vision_backbone)}" + + def forward_image( + self, + samples, + *, + need_sam3_out: bool = True, + need_interactive_out: bool = True, + need_propagation_out: bool = True, + ): + return activation_ckpt_wrapper(self._forward_image_tri_no_act_ckpt)( + samples=samples, + need_sam3_out=need_sam3_out, + need_interactive_out=need_interactive_out, + need_propagation_out=need_propagation_out, + act_ckpt_enable=self.act_ckpt_whole_vision_backbone and self.training, + ) + + def _forward_image_tri_no_act_ckpt( + self, + samples, + need_sam3_out=True, + need_interactive_out=True, + need_propagation_out=True, + ): + ( + sam3_features, + sam3_pos, + interactive_features, + interactive_pos, + propagation_features, + propagation_pos, + ) = self.vision_backbone.forward( + samples, + need_sam3_out=need_sam3_out, + need_interactive_out=need_interactive_out, + need_propagation_out=need_propagation_out, + ) + if self.scalp > 0: + sam3_features, sam3_pos = ( + sam3_features[: -self.scalp], + sam3_pos[: -self.scalp], + ) + interactive_features, interactive_pos = ( + interactive_features[: -self.scalp], + interactive_pos[: -self.scalp], + ) + propagation_features, propagation_pos = ( + propagation_features[: -self.scalp], + propagation_pos[: -self.scalp], + ) + + output = {} + if need_sam3_out: + sam3_last = sam3_features[-1] + output.update( + { + "vision_features": sam3_last.tensors, + "vision_mask": sam3_last.mask, + "vision_pos_enc": sam3_pos, + "backbone_fpn": sam3_features, + } + ) + if need_interactive_out: + inte_last = interactive_features[-1] + output["interactive"] = { + "vision_features": inte_last.tensors, + "vision_mask": inte_last.mask, + "vision_pos_enc": interactive_pos, + "backbone_fpn": interactive_features, + } + if need_propagation_out: + prop_last = propagation_features[-1] + output["sam2_backbone_out"] = { + "vision_features": prop_last.tensors, + "vision_mask": prop_last.mask, + "vision_pos_enc": propagation_pos, + "backbone_fpn": propagation_features, + } + return output + + +class VisionOnly(nn.Module): + def __init__( + self, + visual, + n_features, + forward_in_chunk_for_eval=False, + eval_chunk_size=4, + eval_cast_to_cpu=False, + scalp=0, + compile_mode: str = None, + compile_extra_args: Optional[dict] = None, + ): + super().__init__() + self.vision_backbone = visual + self.should_compile = compile_mode is not None or compile_extra_args is not None + self.compile_mode = compile_mode + self.compile_extra_args = compile_extra_args or {} + self.compiled = False + self.n_features = n_features + self.forward_in_chunk_for_eval = forward_in_chunk_for_eval + self.eval_chunk_size = eval_chunk_size + self.eval_cast_to_cpu = eval_cast_to_cpu + self.scalp = scalp + + def _compile(self): + if self.should_compile and not self.compiled: + self.vision_backbone = torch.compile( + self.vision_backbone, mode=self.compile_mode, **self.compile_extra_args + ) + self.compiled = True + + def forward_image(self, samples): + self._compile() + # Forward through backbone + features, pos = self.vision_backbone(samples) + if self.scalp > 0: + features, pos = features[: -self.scalp], pos[: -self.scalp] + elif self.scalp < 0: + features.pop(self.scalp) + pos.pop(self.scalp) + + src, mask = features[-1].decompose() + output = { + "vision_features": src, + "vision_mask": mask, + "vision_pos_enc": pos, + "backbone_fpn": features, + } + return output + + def forward_text( + self, + captions, + input_boxes=None, + additional_text=None, + device="cuda", + ): + bs = len(captions) + output = { + "language_features": torch.zeros((0, bs, self.n_features), device=device), + "language_mask": torch.zeros((bs, 0), device=device), + } + return output + + +class TriHeadVisionOnly(VisionOnly): + def __init__( + self, + visual, + n_features, + forward_in_chunk_for_eval=False, + eval_chunk_size=4, + eval_cast_to_cpu=False, + scalp=0, + compile_mode: str = None, + compile_extra_args: Optional[dict] = None, + ): + super().__init__( + visual=visual, + n_features=n_features, + forward_in_chunk_for_eval=forward_in_chunk_for_eval, + eval_chunk_size=eval_chunk_size, + eval_cast_to_cpu=eval_cast_to_cpu, + scalp=scalp, + compile_mode=compile_mode, + compile_extra_args=compile_extra_args, + ) + assert isinstance( + self.vision_backbone, Sam3TriViTDetNeck + ), f"Expected vision backbone to be of type Sam3TriViTDetNeck, got {type(self.vision_backbone)}" + + def forward_image( + self, + samples, + *, + need_sam3_out: bool = True, + need_interactive_out: bool = True, + need_propagation_out: bool = True, + ): + self._compile() + # Forward through backbone + ( + sam3_features, + sam3_pos, + interactive_features, + interactive_pos, + propagation_features, + propagation_pos, + ) = self.vision_backbone( + samples, + need_sam3_out=need_sam3_out, + need_interactive_out=need_interactive_out, + need_propagation_out=need_propagation_out, + ) + + if self.scalp > 0: + sam3_features, sam3_pos = ( + sam3_features[: -self.scalp], + sam3_pos[: -self.scalp], + ) + interactive_features, interactive_pos = ( + interactive_features[: -self.scalp], + interactive_pos[: -self.scalp], + ) + propagation_features, propagation_pos = ( + propagation_features[: -self.scalp], + propagation_pos[: -self.scalp], + ) + + output = {} + + if need_sam3_out: + sam3_last = sam3_features[-1] + output.update( + { + "vision_features": sam3_last.tensors, + "vision_mask": sam3_last.mask, + "vision_pos_enc": sam3_pos, + "backbone_fpn": sam3_features, + } + ) + if need_interactive_out: + inte_last = interactive_features[-1] + output["interactive"] = { + "vision_features": inte_last.tensors, + "vision_mask": inte_last.mask, + "vision_pos_enc": interactive_pos, + "backbone_fpn": interactive_features, + } + if need_propagation_out: + prop_last = propagation_features[-1] + output["sam2_backbone_out"] = { + "vision_features": prop_last.tensors, + "vision_mask": prop_last.mask, + "vision_pos_enc": propagation_pos, + "backbone_fpn": propagation_features, + } + + return output diff --git a/sam3/model_builder.py b/sam3/model_builder.py index 103b324..a5dea87 100644 --- a/sam3/model_builder.py +++ b/sam3/model_builder.py @@ -11,10 +11,13 @@ from huggingface_hub import hf_hub_download from iopath.common.file_io import g_pathmgr from sam3.model.decoder import ( + DecoupledTransformerDecoderLayerv2, + SimpleRoPEAttention, TransformerDecoder, TransformerDecoderLayer, TransformerDecoderLayerv2, TransformerEncoderCrossAttention, + TransformerEncoderDecoupledCrossAttention, ) from sam3.model.encoder import TransformerEncoderFusion, TransformerEncoderLayer from sam3.model.geometry_encoders import SequenceGeometryEncoder @@ -31,7 +34,8 @@ MultiheadAttentionWrapper as MultiheadAttention, TransformerWrapper, ) -from sam3.model.necks import Sam3DualViTDetNeck +from sam3.model.multiplex_utils import MultiplexController +from sam3.model.necks import Sam3DualViTDetNeck, Sam3TriViTDetNeck from sam3.model.position_encoding import PositionEmbeddingSine from sam3.model.sam1_task_predictor import SAM3InteractiveImagePredictor from sam3.model.sam3_image import Sam3Image, Sam3ImageOnVideoMultiGPU @@ -40,8 +44,9 @@ from sam3.model.sam3_video_predictor import Sam3VideoPredictorMultiGPU from sam3.model.text_encoder_ve import VETextEncoder from sam3.model.tokenizer_ve import SimpleTokenizer +from sam3.model.video_tracking_multiplex import VideoTrackingDynamicMultiplex from sam3.model.vitdet import ViT -from sam3.model.vl_combiner import SAM3VLBackbone +from sam3.model.vl_combiner import SAM3VLBackbone, SAM3VLBackboneTri, TriHeadVisionOnly from sam3.sam.transformer import RoPEAttention @@ -69,7 +74,7 @@ def _create_position_encoding(precompute_resolution=None): ) -def _create_vit_backbone(compile_mode=None): +def _create_vit_backbone(compile_mode=None, use_fa3=False, use_rope_real=False): """Create ViT backbone for visual feature extraction.""" return ViT( img_size=1008, @@ -96,6 +101,8 @@ def _create_vit_backbone(compile_mode=None): return_interm_layers=False, bias_patch_embed=False, compile_mode=compile_mode, + use_fa3=use_fa3, + use_rope_real=use_rope_real, ) @@ -115,7 +122,7 @@ def _create_vl_backbone(vit_neck, text_encoder): return SAM3VLBackbone(visual=vit_neck, text=text_encoder, scalp=1) -def _create_transformer_encoder() -> TransformerEncoderFusion: +def _create_transformer_encoder(use_fa3=False) -> TransformerEncoderFusion: """Create transformer encoder with its layer.""" encoder_layer = TransformerEncoderLayer( activation="relu", @@ -131,12 +138,14 @@ def _create_transformer_encoder() -> TransformerEncoderFusion: dropout=0.1, embed_dim=256, batch_first=True, + use_fa3=use_fa3, ), cross_attention=MultiheadAttention( num_heads=8, dropout=0.1, embed_dim=256, batch_first=True, + use_fa3=use_fa3, ), ) @@ -153,7 +162,7 @@ def _create_transformer_encoder() -> TransformerEncoderFusion: return encoder -def _create_transformer_decoder() -> TransformerDecoder: +def _create_transformer_decoder(use_fa3=False) -> TransformerDecoder: """Create transformer decoder with its layer.""" decoder_layer = TransformerDecoderLayer( activation="relu", @@ -164,6 +173,7 @@ def _create_transformer_decoder() -> TransformerDecoder: num_heads=8, dropout=0.1, embed_dim=256, + use_fa3=use_fa3, ), n_heads=8, use_text_cross_attention=True, @@ -204,7 +214,7 @@ def _create_dot_product_scoring(): return DotProductScoring(d_model=256, d_proj=256, prompt_mlp=prompt_mlp) -def _create_segmentation_head(compile_mode=None): +def _create_segmentation_head(compile_mode=None, use_fa3=False): """Create segmentation head with pixel decoder.""" pixel_decoder = PixelDecoder( num_upsampling_stages=3, @@ -217,6 +227,7 @@ def _create_segmentation_head(compile_mode=None): num_heads=8, dropout=0, embed_dim=256, + use_fa3=use_fa3, ) segmentation_head = UniversalSegmentationHead( @@ -515,10 +526,12 @@ def _create_vision_backbone( return vit_neck -def _create_sam3_transformer(has_presence_token: bool = True) -> TransformerWrapper: +def _create_sam3_transformer( + has_presence_token: bool = True, use_fa3: bool = False +) -> TransformerWrapper: """Create SAM3 transformer encoder and decoder.""" - encoder: TransformerEncoderFusion = _create_transformer_encoder() - decoder: TransformerDecoder = _create_transformer_decoder() + encoder: TransformerEncoderFusion = _create_transformer_encoder(use_fa3=use_fa3) + decoder: TransformerDecoder = _create_transformer_decoder(use_fa3=use_fa3) return TransformerWrapper(encoder=encoder, decoder=decoder, d_model=256) @@ -630,7 +643,7 @@ def build_sam3_image_model( eval_mode, ) if load_from_HF and checkpoint_path is None: - checkpoint_path = download_ckpt_from_hf() + checkpoint_path = download_ckpt_from_hf(version="sam3") # Load checkpoint if provided if checkpoint_path is not None: _load_checkpoint(model, checkpoint_path) @@ -641,12 +654,22 @@ def build_sam3_image_model( return model -def download_ckpt_from_hf(): - SAM3_MODEL_ID = "facebook/sam3" - SAM3_CKPT_NAME = "sam3.pt" - SAM3_CFG_NAME = "config.json" - _ = hf_hub_download(repo_id=SAM3_MODEL_ID, filename=SAM3_CFG_NAME) - checkpoint_path = hf_hub_download(repo_id=SAM3_MODEL_ID, filename=SAM3_CKPT_NAME) +def download_ckpt_from_hf(version="sam3"): + """Download model checkpoint from HuggingFace Hub. + + Args: + version: "sam3" or "sam3.1" + """ + if version == "sam3.1": + repo_id = "facebook/sam3.1" + ckpt_name = "sam3.1_multiplex.pt" + cfg_name = "config.json" + else: + repo_id = "facebook/sam3" + ckpt_name = "sam3.pt" + cfg_name = "config.json" + _ = hf_hub_download(repo_id=repo_id, filename=cfg_name) + checkpoint_path = hf_hub_download(repo_id=repo_id, filename=ckpt_name) return checkpoint_path @@ -772,7 +795,7 @@ def build_sam3_video_model( # Load checkpoint if provided if load_from_HF and checkpoint_path is None: - checkpoint_path = download_ckpt_from_hf() + checkpoint_path = download_ckpt_from_hf(version="sam3") if checkpoint_path is not None: with g_pathmgr.open(checkpoint_path, "rb") as f: ckpt = torch.load(f, map_location="cpu", weights_only=True) @@ -795,3 +818,501 @@ def build_sam3_video_predictor(*model_args, gpus_to_use=None, **model_kwargs): return Sam3VideoPredictorMultiGPU( *model_args, gpus_to_use=gpus_to_use, **model_kwargs ) + + +def _create_multiplex_maskmem_backbone(multiplex_count=16): + """Create the multiplex memory encoder with per-object mask channels.""" + position_encoding = PositionEmbeddingSine( + num_pos_feats=256, + normalize=True, + scale=None, + temperature=10000, + precompute_resolution=1008, + ) + + mask_downsampler = SimpleMaskDownSampler( + kernel_size=3, + stride=2, + padding=1, + interpol_size=[1152, 1152], + multiplex_count=multiplex_count, + starting_out_chan=4, + input_channel_multiplier=2, + ) + + cx_block_layer = CXBlock( + dim=256, + kernel_size=7, + padding=3, + layer_scale_init_value=1.0e-06, + use_dwconv=True, + ) + + fuser = SimpleFuser(layer=cx_block_layer, num_layers=2) + + maskmem_backbone = SimpleMaskEncoder( + out_dim=256, + position_encoding=position_encoding, + mask_downsampler=mask_downsampler, + fuser=fuser, + ) + + return maskmem_backbone + + +def _create_multiplex_transformer(use_fa3=False, use_rope_real=False): + """Create the decoupled transformer for multiplex memory attention.""" + self_attention_rope = SimpleRoPEAttention( + d_model=256, + num_heads=8, + dropout_p=0.1, + rope_theta=10000.0, + feat_sizes=[72, 72], + use_fa3=use_fa3, + use_rope_real=use_rope_real, + ) + + cross_attention_rope = SimpleRoPEAttention( + d_model=256, + num_heads=8, + dropout_p=0.1, + rope_theta=10000.0, + feat_sizes=[72, 72], + rope_k_repeat=True, + use_fa3=use_fa3, + use_rope_real=use_rope_real, + ) + + encoder_layer = DecoupledTransformerDecoderLayerv2( + activation="gelu", + d_model=256, + num_heads=8, + dropout=0.1, + dim_feedforward=2048, + pos_enc_at_attn=False, + pre_norm=True, + pos_enc_at_cross_attn_keys=True, + pos_enc_at_cross_attn_queries=False, + self_attention_rope=self_attention_rope, + cross_attention_rope=cross_attention_rope, + ) + + encoder = TransformerEncoderDecoupledCrossAttention( + d_model=256, + frozen=False, + pos_enc_at_input=True, + use_image_in_output=False, + layer=encoder_layer, + num_layers=4, + use_act_checkpoint=False, + batch_first=True, + ) + + transformer = TransformerWrapper( + encoder=encoder, + decoder=None, + d_model=256, + ) + + return transformer + + +def _create_multiplex_tri_backbone( + compile_mode=None, use_fa3=False, use_rope_real=False +): + """Create the TriHead vision backbone for multiplex model.""" + position_encoding = _create_position_encoding(precompute_resolution=1008) + vit_backbone = _create_vit_backbone( + compile_mode=compile_mode, use_fa3=use_fa3, use_rope_real=use_rope_real + ) + tri_neck = Sam3TriViTDetNeck( + trunk=vit_backbone, + position_encoding=position_encoding, + d_model=256, + scale_factors=[4.0, 2.0, 1.0], + ) + return tri_neck + + +def build_sam3_multiplex_video_model( + checkpoint_path: Optional[str] = None, + load_from_HF=True, + multiplex_count: int = 16, + use_fa3: bool = False, + use_rope_real: bool = False, + strict_state_dict_loading: bool = True, + device="cuda" if torch.cuda.is_available() else "cpu", + compile=False, +): + """ + Build SAM3 multiplex video tracking model. + + Args: + checkpoint_path: Optional path to checkpoint file + multiplex_count: Number of objects per multiplex bucket + use_fa3: Whether to use FlashAttention 3 + use_rope_real: Whether to use real-valued RoPE (for compile compat) + strict_state_dict_loading: Whether to use strict state dict loading + device: Device to place model on + compile: Whether to compile model components + + Returns: + VideoTrackingDynamicMultiplex: The instantiated multiplex tracking model + """ + # Build multiplex-specific components + maskmem_backbone = _create_multiplex_maskmem_backbone( + multiplex_count=multiplex_count + ) + transformer = _create_multiplex_transformer( + use_fa3=use_fa3, use_rope_real=use_rope_real + ) + tri_neck = _create_multiplex_tri_backbone( + compile_mode="max-autotune" if compile else None + ) + backbone = TriHeadVisionOnly( + visual=tri_neck, + n_features=256, + scalp=0, + ) + multiplex_controller = MultiplexController( + multiplex_count=multiplex_count, + eval_multiplex_count=multiplex_count, + ) + + # Build the multiplex model (use demo class for init_state and other demo methods) + from sam3.model.video_tracking_multiplex_demo import Sam3VideoTrackingMultiplexDemo + + model = Sam3VideoTrackingMultiplexDemo( + backbone=backbone, + transformer=transformer, + maskmem_backbone=maskmem_backbone, + multiplex_controller=multiplex_controller, + image_size=1008, + backbone_stride=14, + num_maskmem=7, + # Multiplex-specific settings + use_high_res_features_in_sam=True, + use_obj_ptrs_in_encoder=True, + max_obj_ptrs_in_encoder=16, + add_tpos_enc_to_obj_ptrs=True, + proj_tpos_enc_in_obj_ptrs=True, + use_mlp_for_obj_ptr_proj=True, + pred_obj_scores=True, + pred_obj_scores_mlp=True, + fixed_no_obj_ptr=True, + use_no_obj_ptr=True, + use_linear_no_obj_ptr=True, + no_obj_embed_spatial=True, + sincos_tpos_enc=True, + # Multimask settings + multimask_output_in_sam=True, + multimask_output_for_tracking=True, + multimask_min_pt_num=0, + multimask_max_pt_num=1, + use_multimask_token_for_obj_ptr=True, + num_multimask_outputs=3, + # Memory encoder settings + apply_sigmoid_to_mask_logits_for_mem_enc=True, + sigmoid_scale_for_mem_enc=2.0, + sigmoid_bias_for_mem_enc=-1.0, + non_overlap_masks_for_mem_enc=False, + # Suppression/conditional embeddings + add_output_suppression_embeddings=True, + add_object_conditional_embeddings=False, + condition_as_mask_input=True, + condition_as_mask_input_fg=1.0, + condition_as_mask_input_bg=0.0, + # Memory settings + use_maskmem_tpos_v2=True, + save_image_features=True, + randomness_fix=True, + # Interaction settings + use_mask_input_as_output_without_sam=True, + directly_add_no_mem_embed=True, + iou_prediction_use_sigmoid=False, + forward_backbone_per_frame_for_eval=True, + offload_output_to_cpu_for_eval=False, + trim_past_non_cond_mem_for_eval=False, + max_cond_frames_in_attn=4, + # Dynamic multiplex settings + is_dynamic_model=True, + # SAM mask decoder extra args + sam_mask_decoder_extra_args={ + "dynamic_multimask_via_stability": True, + "dynamic_multimask_stability_delta": 0.05, + "dynamic_multimask_stability_thresh": 0.98, + }, + compile_all_components=compile, + use_memory_selection=False, + ) + + # Load checkpoint if provided + if load_from_HF and checkpoint_path is None: + checkpoint_path = download_ckpt_from_hf(version="sam3.1") + if checkpoint_path is not None: + with g_pathmgr.open(checkpoint_path, "rb") as f: + ckpt = torch.load(f, map_location="cpu", weights_only=True) + if "model" in ckpt and isinstance(ckpt["model"], dict): + ckpt = ckpt["model"] + + missing_keys, unexpected_keys = model.load_state_dict( + ckpt, strict=strict_state_dict_loading + ) + if missing_keys: + print(f"Missing keys: {missing_keys}") + if unexpected_keys: + print(f"Unexpected keys: {unexpected_keys}") + + model.to(device=device) + return model + + +def build_sam3_multiplex_video_predictor( + checkpoint_path: Optional[str] = None, + bpe_path: Optional[str] = None, + max_num_objects: int = 16, + multiplex_count: int = 16, + use_fa3: bool = True, + use_rope_real: bool = True, + compile: bool = False, + warm_up: bool = False, + session_expiration_sec: int = 1200, + default_output_prob_thresh: float = 0.5, + async_loading_frames: bool = True, +): + """ + Build a fully-initialized Sam3MultiplexVideoPredictor. + + This is the recommended entry point for SAM 3.1 multiplex video tracking. + It builds the full model stack (tracker + detector + demo model), loads + the checkpoint, and wraps everything in Sam3MultiplexVideoPredictor with + handle_request / handle_stream_request API. + + Args: + checkpoint_path: Path to the merged multiplex checkpoint + bpe_path: Path to the BPE tokenizer vocabulary + max_num_objects: Maximum number of tracked objects + multiplex_count: Number of objects per multiplex bucket + use_fa3: Whether to use FlashAttention 3 + use_rope_real: Whether to use real-valued RoPE (for compile compat) + compile: Whether to enable torch.compile on model components + warm_up: Whether to run warm-up compilation (requires compile=True) + session_expiration_sec: Session expiration timeout in seconds + default_output_prob_thresh: Default probability threshold for output masks + async_loading_frames: Whether to load frames asynchronously + + Returns: + Sam3MultiplexVideoPredictor: The fully-initialized predictor + """ + if bpe_path is None: + bpe_path = pkg_resources.resource_filename( + "sam3", "assets/bpe_simple_vocab_16e6.txt.gz" + ) + + from sam3.model.sam3_multiplex_base import Sam3MultiplexPredictorWrapper + from sam3.model.sam3_multiplex_detector import Sam3MultiplexDetector + from sam3.model.sam3_multiplex_tracking import ( + Sam3MultiplexTrackingWithInteractivity, + ) + from sam3.model.sam3_multiplex_video_predictor import Sam3MultiplexVideoPredictor + + # Build tracker + tracker_model = build_sam3_multiplex_video_model( + checkpoint_path=checkpoint_path, + load_from_HF=False, + multiplex_count=multiplex_count, + use_fa3=use_fa3, + use_rope_real=use_rope_real, + compile=False, + strict_state_dict_loading=False, + ) + del tracker_model.backbone + tracker_model.backbone = None + + sam2_predictor = Sam3MultiplexPredictorWrapper( + model=tracker_model, + per_obj_inference=False, + fill_hole_area=0, + is_multiplex=True, + is_multiplex_dynamic=True, + ) + + # Build detector + tri_neck = _create_multiplex_tri_backbone( + compile_mode=None, use_fa3=use_fa3, use_rope_real=use_rope_real + ) + text_encoder = _create_text_encoder(bpe_path) + backbone = SAM3VLBackboneTri(scalp=0, visual=tri_neck, text=text_encoder) + transformer = _create_sam3_transformer(use_fa3=use_fa3) + segmentation_head = _create_segmentation_head(use_fa3=use_fa3) + geometry_encoder = _create_geometry_encoder() + dot_prod_scoring = _create_dot_product_scoring() + + detector = Sam3MultiplexDetector( + num_feature_levels=1, + backbone=backbone, + transformer=transformer, + segmentation_head=segmentation_head, + semantic_segmentation_head=None, + input_geometry_encoder=geometry_encoder, + use_early_fusion=True, + use_dot_prod_scoring=True, + dot_prod_scoring=dot_prod_scoring, + supervise_joint_box_scores=True, + is_multiplex=True, + ) + + # Assemble demo model + demo_model = Sam3MultiplexTrackingWithInteractivity( + tracker=sam2_predictor, + detector=detector, + score_threshold_detection=0.4, + det_nms_thresh=0.1, + det_nms_use_iom=True, + assoc_iou_thresh=0.1, + new_det_thresh=0.65, + hotstart_delay=15, + hotstart_unmatch_thresh=8, + hotstart_dup_thresh=8, + suppress_unmatched_only_within_hotstart=False, + suppress_overlapping_based_on_recent_occlusion_threshold=0.7, + suppress_det_close_to_boundary=True, + fill_hole_area=0, # OV effectively 0 (Sam3MultiplexTrackerPredictor Hydra override clobbers yaml's 16) + recondition_every_nth_frame=16, + use_iom_recondition=True, + iom_thresh_recondition=0.5, + masklet_confirmation_enable=True, + reconstruction_bbox_iou_thresh=-1, + reconstruction_bbox_det_score=0.8, + max_num_objects=max_num_objects, + postprocess_batch_size=16, + use_batched_grounding=True, + batched_grounding_batch_size=16, + max_num_kboxes=0, + sprinkle_removal_area=0, + is_multiplex=True, + image_size=1008, + image_mean=(0.5, 0.5, 0.5), + image_std=(0.5, 0.5, 0.5), + compile_model=compile, + ) + + # Load checkpoint (auto-download from HuggingFace if not provided) + if checkpoint_path is None: + checkpoint_path = download_ckpt_from_hf(version="sam3.1") + if checkpoint_path is not None: + ckpt = torch.load(checkpoint_path, map_location="cpu", weights_only=True) + if "model" in ckpt and isinstance(ckpt["model"], dict): + ckpt = ckpt["model"] + # Remap checkpoint keys if needed (internal naming -> OSS naming) + # HF checkpoints are already remapped; local checkpoints may use old naming + needs_remap = any( + k.startswith("sam3_model.") or k.startswith("sam2_predictor.") for k in ckpt + ) + if needs_remap: + remapped_ckpt = {} + for k, v in ckpt.items(): + new_k = k + if k.startswith("sam3_model."): + new_k = "detector." + k[len("sam3_model.") :] + elif k.startswith("sam2_predictor."): + new_k = "tracker." + k[len("sam2_predictor.") :] + remapped_ckpt[new_k] = v + ckpt = remapped_ckpt + missing_keys, unexpected_keys = demo_model.load_state_dict(ckpt, strict=False) + if missing_keys: + print(f"Missing keys ({len(missing_keys)}): {missing_keys[:10]}...") + if unexpected_keys: + print( + f"Unexpected keys ({len(unexpected_keys)}): {unexpected_keys[:10]}..." + ) + + demo_model.cuda().eval() + + # Wrap in predictor + predictor = Sam3MultiplexVideoPredictor( + model=demo_model, + session_expiration_sec=session_expiration_sec, + default_output_prob_thresh=default_output_prob_thresh, + async_loading_frames=async_loading_frames, + warm_up=warm_up, + ) + return predictor + + +def build_sam3_predictor( + checkpoint_path: Optional[str] = None, + bpe_path: Optional[str] = None, + version: str = "sam3.1", # "sam3" or "sam3.1" + compile: bool = False, + warm_up: bool = False, + # SAM 3.1 specific + max_num_objects: int = 16, + multiplex_count: int = 16, + # Common + use_fa3: bool = True, + use_rope_real: bool = True, + async_loading_frames: bool = True, + **kwargs, +): + """ + Build a SAM3 video predictor. + + Args: + checkpoint_path: Path to model checkpoint + bpe_path: Path to BPE tokenizer vocabulary + version: Model version - "sam3" for base or "sam3.1" for multiplex + compile: Enable torch.compile for ~2x speedup (SAM 3.1 only currently) + warm_up: Run warm-up compilation passes + max_num_objects: Maximum tracked objects (SAM 3.1 only) + multiplex_count: Objects per multiplex bucket (SAM 3.1 only) + use_fa3: Use Flash Attention 3 + use_rope_real: Use real-valued RoPE + async_loading_frames: Load video frames asynchronously + **kwargs: Additional arguments passed to the underlying builder + + Returns: + A predictor with handle_request() and handle_stream_request() API. + Both versions support: start_session, add_prompt, propagate_in_video, + remove_object, reset_session, close_session. + + Example: + # SAM 3.1 (auto-downloads from HuggingFace): + predictor = build_sam3_predictor(version="sam3.1", compile=True) + + # SAM 3 (auto-downloads from HuggingFace): + predictor = build_sam3_predictor(version="sam3") + + # Or with a local checkpoint: + predictor = build_sam3_predictor(checkpoint_path="path/to/ckpt.pt", version="sam3.1") + + # Both use the same API: + response = predictor.handle_request({"type": "start_session", "resource_path": video_dir}) + session_id = response["session_id"] + predictor.handle_request({"type": "add_prompt", "session_id": session_id, "frame_index": 0, "text": "person"}) + for out in predictor.handle_stream_request({"type": "propagate_in_video", "session_id": session_id}): + masks = out["out_binary_masks"] + """ + if version == "sam3.1": + return build_sam3_multiplex_video_predictor( + checkpoint_path=checkpoint_path, + bpe_path=bpe_path, + max_num_objects=max_num_objects, + multiplex_count=multiplex_count, + use_fa3=use_fa3, + use_rope_real=use_rope_real, + compile=compile, + warm_up=warm_up, + async_loading_frames=async_loading_frames, + **kwargs, + ) + elif version == "sam3": + return build_sam3_video_predictor( + checkpoint_path=checkpoint_path, + bpe_path=bpe_path, + compile=compile, + async_loading_frames=async_loading_frames, + **kwargs, + ) + else: + raise ValueError(f"Unknown version: {version!r}. Use 'sam3' or 'sam3.1'.") diff --git a/sam3/perflib/compile.py b/sam3/perflib/compile.py index 914c31b..8471406 100644 --- a/sam3/perflib/compile.py +++ b/sam3/perflib/compile.py @@ -2,7 +2,11 @@ # pyre-unsafe +from functools import wraps + import torch +from sam3.model.data_misc import BatchedDatapoint, NestedTensor +from torch.utils._pytree import tree_map_only def recursive_fn_factory(fn): @@ -13,15 +17,18 @@ def recursive_fn(b): return [recursive_fn(t) for t in b] if isinstance(b, tuple): return tuple(recursive_fn(t) for t in b) + if isinstance(b, NestedTensor): + tensors = fn(b.tensors) + if b.mask is None: + mask = None + else: + mask = fn(b.mask) + return NestedTensor(tensors=tensors, mask=mask) if isinstance(b, torch.Tensor): return fn(b) - # Yes, writing out an explicit white list of - # trivial types is tedious, but so are bugs that - # come from not applying fn, when expected to have - # applied it. if b is None: return b - trivial_types = [bool, int] + trivial_types = [bool, int, float] for t in trivial_types: if isinstance(b, t): return b @@ -34,20 +41,41 @@ def recursive_fn(b): recursive_clone = recursive_fn_factory(torch.clone) +def clone_output_wrapper(f): + """ + Clone the CUDA output tensors of a function to avoid in-place operations. + Uses tree_map_only (C-optimized pytree traversal) matching onevision's pattern. + Requires NestedTensor to be registered as a pytree node (see data_misc.py). + """ + + @wraps(f) + def wrapped(*args, **kwargs): + outputs = f(*args, **kwargs) + return tree_map_only( + torch.Tensor, lambda t: t.clone() if t.is_cuda else t, outputs + ) + + return wrapped + + def compile_wrapper( fn, *, mode="max-autotune", fullgraph=True, dynamic=False, name=None ): + """Compile with recursive_contiguous on inputs and recursive_clone on outputs. + Used for SAM2 tracker components that need contiguous inputs for CUDA graphs.""" compiled_fn = torch.compile(fn, mode=mode, fullgraph=fullgraph, dynamic=dynamic) def compiled_fn_wrapper(*args, **kwargs): with torch.autograd.profiler.record_function( f"compiled {fn}" if name is None else name ): - cont_args = recursive_contiguous(args) - cont_kwargs = recursive_contiguous(kwargs) - result = compiled_fn(*cont_args, **cont_kwargs) - cloned_result = recursive_clone(result) - return cloned_result + CUDAGRAPH_MODES = ["max-autotune", "reduce-overhead"] + args = recursive_contiguous(args) + kwargs = recursive_contiguous(kwargs) + result = compiled_fn(*args, **kwargs) + if mode in CUDAGRAPH_MODES: + result = recursive_clone(result) + return result return compiled_fn_wrapper @@ -56,11 +84,6 @@ def shape_logging_wrapper(fn, keep_kwargs, enable_logging=False): """ Wraps a function and prints the shapes of all tensor inputs. Only prints when a new combination of shapes is seen. - Thread-safe. - - Args: - fn: Function to wrap - enable_logging: Boolean flag to enable/disable logging """ seen_shapes = set() @@ -89,7 +112,6 @@ def wrapper(*args, **kwargs): print(f"[ShapeLogger] New input shapes for {fn.__qualname__}: {shapes}") return fn(*args, **kwargs) - # Allow toggling the flag at runtime wrapper.enable_logging = enable_logging def set_logging(enabled=False): diff --git a/sam3/perflib/connected_components.py b/sam3/perflib/connected_components.py index fa0506d..0be67e5 100644 --- a/sam3/perflib/connected_components.py +++ b/sam3/perflib/connected_components.py @@ -36,9 +36,9 @@ def connected_components_cpu(input_tensor: torch.Tensor): if input_tensor.dim() == 4 and input_tensor.shape[1] == 1: input_tensor = input_tensor.squeeze(1) else: - assert input_tensor.dim() == 3, ( - "Input tensor must be (B, H, W) or (B, 1, H, W)." - ) + assert ( + input_tensor.dim() == 3 + ), "Input tensor must be (B, H, W) or (B, 1, H, W)." batch_size = input_tensor.shape[0] labels_list = [] @@ -67,9 +67,9 @@ def connected_components(input_tensor: torch.Tensor): if input_tensor.dim() == 3: input_tensor = input_tensor.unsqueeze(1) - assert input_tensor.dim() == 4 and input_tensor.shape[1] == 1, ( - "Input tensor must be (B, H, W) or (B, 1, H, W)." - ) + assert ( + input_tensor.dim() == 4 and input_tensor.shape[1] == 1 + ), "Input tensor must be (B, H, W) or (B, 1, H, W)." if input_tensor.is_cuda: if HAS_CC_TORCH: diff --git a/sam3/perflib/fused.py b/sam3/perflib/fused.py new file mode 100644 index 0000000..6800cca --- /dev/null +++ b/sam3/perflib/fused.py @@ -0,0 +1,25 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved + +# pyre-unsafe + +import torch + +addmm_act_op = torch.ops.aten._addmm_activation + + +def addmm_act(activation, linear, mat1): + if torch.is_grad_enabled(): + raise ValueError("Expected grad to be disabled.") + self = linear.bias.detach() + mat2 = linear.weight.detach() + self = self.to(torch.bfloat16) + mat1 = mat1.to(torch.bfloat16) + mat2 = mat2.to(torch.bfloat16) + mat1_flat = mat1.view(-1, mat1.shape[-1]) + if activation in [torch.nn.functional.relu, torch.nn.ReLU]: + y = addmm_act_op(self, mat1_flat, mat2.t(), beta=1, alpha=1, use_gelu=False) + return y.view(mat1.shape[:-1] + (y.shape[-1],)) + if activation in [torch.nn.functional.gelu, torch.nn.GELU]: + y = addmm_act_op(self, mat1_flat, mat2.t(), beta=1, alpha=1, use_gelu=True) + return y.view(mat1.shape[:-1] + (y.shape[-1],)) + raise ValueError(f"Unexpected activation {activation}") diff --git a/sam3/perflib/iou.py b/sam3/perflib/iou.py new file mode 100644 index 0000000..2b32f80 --- /dev/null +++ b/sam3/perflib/iou.py @@ -0,0 +1,38 @@ +import torch + + +def pairwise_iou(pred_masks, gt_masks, eps=1e-6): + N, H, W = pred_masks.shape + M = gt_masks.shape[0] + # Flatten and convert to float for matmul + pred_flat = pred_masks.reshape(N, -1).float() + gt_flat = gt_masks.reshape(M, -1).float() + # Intersection: (N, M) + intersection = torch.matmul(pred_flat, gt_flat.t()) + # Areas + area_pred = pred_flat.sum(dim=1, keepdim=True) # (N, 1) + area_gt = gt_flat.sum(dim=1, keepdim=True) # (M, 1) + # Union: (N, M) + union = area_pred + area_gt.t() - intersection + if eps is None: + iou = intersection / union.clamp(min=1) + else: + iou = intersection / (union + eps) + return iou # shape: (N, M) + + +def pairwise_iom(pred_masks, gt_masks, eps=1e-8): + N, H, W = pred_masks.shape + M = gt_masks.shape[0] + # Flatten and convert to float for matmul + pred_flat = pred_masks.reshape(N, -1).float() + gt_flat = gt_masks.reshape(M, -1).float() + # Intersection: (N, M) + intersection = torch.matmul(pred_flat, gt_flat.t()) + # Areas + area_pred = pred_flat.sum(dim=1, keepdim=True) # (N, 1) + area_gt = gt_flat.sum(dim=1, keepdim=True) # (M, 1) + # Union: (N, M) + min_area = torch.min(area_pred, area_gt) + iou = intersection / (min_area + eps) + return iou # shape: (N, M) diff --git a/sam3/perflib/masks_ops.py b/sam3/perflib/masks_ops.py index 7946996..806172d 100644 --- a/sam3/perflib/masks_ops.py +++ b/sam3/perflib/masks_ops.py @@ -50,6 +50,8 @@ def masks_to_boxes(masks: torch.Tensor, obj_ids: list[int]): def mask_iou(pred_masks: torch.Tensor, gt_masks: torch.Tensor) -> torch.Tensor: """ Compute the IoU (Intersection over Union) between predicted masks and ground truth masks. + Uses matmul-based vectorized intersection for Tensor Core acceleration. + Args: - pred_masks: (N, H, W) bool Tensor, containing binary predicted segmentation masks - gt_masks: (M, H, W) bool Tensor, containing binary ground truth segmentation masks @@ -57,15 +59,14 @@ def mask_iou(pred_masks: torch.Tensor, gt_masks: torch.Tensor) -> torch.Tensor: - ious: (N, M) float Tensor, containing IoUs for each pair of predicted and ground truth masks """ assert pred_masks.dtype == gt_masks.dtype == torch.bool - N, H, W = pred_masks.shape - M, _, _ = gt_masks.shape + assert pred_masks.shape[1:] == gt_masks.shape[1:] - # Flatten masks: (N, 1, H*W) and (1, M, H*W) - pred_flat = pred_masks.view(N, 1, H * W) - gt_flat = gt_masks.view(1, M, H * W) + # Matmul-based intersection (uses Tensor Cores via float mm) + m1_flat = pred_masks.flatten(1).float() + m2_flat = gt_masks.flatten(1).float() + intersection = torch.mm(m1_flat, m2_flat.t()) - # Compute intersection and union: (N, M) - intersection = (pred_flat & gt_flat).sum(dim=2).float() - union = (pred_flat | gt_flat).sum(dim=2).float() - ious = intersection / union.clamp(min=1) - return ious # shape: (N, M) + area1 = m1_flat.sum(dim=1) + area2 = m2_flat.sum(dim=1) + union = area1[:, None] + area2[None, :] - intersection + return intersection / union.clamp(min=1) diff --git a/sam3/perflib/triton/connected_components.py b/sam3/perflib/triton/connected_components.py index 1d4376e..cdb7d44 100644 --- a/sam3/perflib/triton/connected_components.py +++ b/sam3/perflib/triton/connected_components.py @@ -407,16 +407,16 @@ def connected_components_triton(input_tensor: torch.Tensor): - A BxHxW output tensor with dense labels. Background is 0. - A BxHxW tensor with the size of the connected component for each pixel. """ - assert input_tensor.is_cuda and input_tensor.is_contiguous(), ( - "Input tensor must be a contiguous CUDA tensor." - ) + assert ( + input_tensor.is_cuda and input_tensor.is_contiguous() + ), "Input tensor must be a contiguous CUDA tensor." out_shape = input_tensor.shape if input_tensor.dim() == 4 and input_tensor.shape[1] == 1: input_tensor = input_tensor.squeeze(1) else: - assert input_tensor.dim() == 3, ( - "Input tensor must be (B, H, W) or (B, 1, H, W)." - ) + assert ( + input_tensor.dim() == 3 + ), "Input tensor must be (B, H, W) or (B, 1, H, W)." B, H, W = input_tensor.shape numel = B * H * W diff --git a/sam3/sam/mask_decoder.py b/sam3/sam/mask_decoder.py index 944e57a..3e1bbd2 100644 --- a/sam3/sam/mask_decoder.py +++ b/sam3/sam/mask_decoder.py @@ -202,9 +202,9 @@ def predict_masks( assert image_embeddings.shape[0] == tokens.shape[0] src = image_embeddings src = src + dense_prompt_embeddings - assert image_pe.size(0) == 1, ( - "image_pe should have size 1 in batch dim (from `get_dense_pe()`)" - ) + assert ( + image_pe.size(0) == 1 + ), "image_pe should have size 1 in batch dim (from `get_dense_pe()`)" pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0) b, c, h, w = src.shape diff --git a/sam3/sam/transformer.py b/sam3/sam/transformer.py index 1ff2380..6716c54 100644 --- a/sam3/sam/transformer.py +++ b/sam3/sam/transformer.py @@ -204,9 +204,9 @@ def __init__( self.internal_dim = embedding_dim // downsample_rate self.num_heads = num_heads self.use_fa3 = use_fa3 - assert self.internal_dim % num_heads == 0, ( - "num_heads must divide embedding_dim." - ) + assert ( + self.internal_dim % num_heads == 0 + ), "num_heads must divide embedding_dim." self.q_proj = nn.Linear(embedding_dim, self.internal_dim) self.k_proj = nn.Linear(self.kv_in_dim, self.internal_dim) diff --git a/sam3/train/data/coco_json_loaders.py b/sam3/train/data/coco_json_loaders.py index d39b6f4..1618e19 100644 --- a/sam3/train/data/coco_json_loaders.py +++ b/sam3/train/data/coco_json_loaders.py @@ -142,9 +142,9 @@ def __init__( self.prompts = {} for loc_dict in prompts: self.prompts[int(loc_dict["id"])] = loc_dict["name"] - assert len(self.prompts) == len(self._sorted_cat_ids), ( - "Number of prompts must match number of categories" - ) + assert len(self.prompts) == len( + self._sorted_cat_ids + ), "Number of prompts must match number of categories" def getDatapointIds(self): """Return all datapoint indices for training.""" diff --git a/sam3/train/data/collator.py b/sam3/train/data/collator.py index 38d031d..8ce4e28 100644 --- a/sam3/train/data/collator.py +++ b/sam3/train/data/collator.py @@ -216,9 +216,9 @@ def collate_fn_api( text_batch.append(q.query_text) stages[stage_id].text_ids.append(text_batch.index(q.query_text)) - assert q.inference_metadata is not None, ( - "inference_metadata must be provided when FindQueryLoaded is created." - ) + assert ( + q.inference_metadata is not None + ), "inference_metadata must be provided when FindQueryLoaded is created." for f in fields(q.inference_metadata): getattr(find_metadatas[stage_id], f.name).append( getattr(q.inference_metadata, f.name) diff --git a/sam3/train/data/sam3_image_dataset.py b/sam3/train/data/sam3_image_dataset.py index 28e7425..dca5b97 100644 --- a/sam3/train/data/sam3_image_dataset.py +++ b/sam3/train/data/sam3_image_dataset.py @@ -233,9 +233,9 @@ def set_sharded_annotation_file(self, data_epoch: int): if self.coco is not None: return - assert g_pathmgr.isfile(self.annFile), ( - f"please provide valid annotation file. Missing: {self.annFile}" - ) + assert g_pathmgr.isfile( + self.annFile + ), f"please provide valid annotation file. Missing: {self.annFile}" annFile = g_pathmgr.get_local_path(self.annFile) if self.coco is not None: @@ -325,9 +325,9 @@ def load_queries(self, pil_images, annotations, queries, img_metadata): else: num_queries_per_stage = stage2num_queries.most_common(1)[0][1] for stage, num_queries in stage2num_queries.items(): - assert num_queries == num_queries_per_stage, ( - f"Number of queries in stage {stage} is {num_queries}, expected {num_queries_per_stage}" - ) + assert ( + num_queries == num_queries_per_stage + ), f"Number of queries in stage {stage} is {num_queries}, expected {num_queries_per_stage}" for query in queries: h, w = id2imsize[query["image_id"]] diff --git a/sam3/train/data/sam3_video_dataset.py b/sam3/train/data/sam3_video_dataset.py index 75eee62..3be37a4 100644 --- a/sam3/train/data/sam3_video_dataset.py +++ b/sam3/train/data/sam3_video_dataset.py @@ -15,6 +15,7 @@ import torch import torchvision + # from decord import cpu, VideoReader from iopath.common.file_io import PathManager @@ -218,9 +219,9 @@ def _filter_query_and_anns( for query in filtered_queries: ptr_x_is_empty = query["ptr_x_query_id"] in [None, -1] ptr_y_is_empty = query["ptr_y_query_id"] in [None, -1] - assert ptr_x_is_empty and ptr_y_is_empty, ( - "Remapping stage ids is not supported for queries with non-empty ptr_x or ptr_y pointers" - ) + assert ( + ptr_x_is_empty and ptr_y_is_empty + ), "Remapping stage ids is not supported for queries with non-empty ptr_x or ptr_y pointers" query["query_processing_order"] = stage_id_old2new[ query["query_processing_order"] ] diff --git a/sam3/train/loss/loss_fns.py b/sam3/train/loss/loss_fns.py index 8fa1774..ca8e348 100644 --- a/sam3/train/loss/loss_fns.py +++ b/sam3/train/loss/loss_fns.py @@ -324,9 +324,7 @@ def __init__( if num_det_queries is not None: logging.warning("note: it's not needed to set num_det_queries anymore") if self.use_separate_loss_for_det_and_trk: - assert not self.weak_loss, ( - "Do not use weak_loss in this case -- set separate loss for detection and tracking queries instead" - ) + assert not self.weak_loss, "Do not use weak_loss in this case -- set separate loss for detection and tracking queries instead" self.det_exhaustive_loss_scale_pos = det_exhaustive_loss_scale_pos self.det_exhaustive_loss_scale_neg = det_exhaustive_loss_scale_neg self.det_non_exhaustive_loss_scale_pos = det_non_exhaustive_loss_scale_pos @@ -341,9 +339,7 @@ def __init__( and det_non_exhaustive_loss_scale_neg == 1.0 and trk_loss_scale_pos == 1.0 and trk_loss_scale_neg == 1.0 - ), ( - "If not using separate loss for detection and tracking queries, separate detection and tracking loss scales should all be 1.0" - ) + ), "If not using separate loss for detection and tracking queries, separate detection and tracking loss scales should all be 1.0" def get_loss(self, outputs, targets, indices, num_boxes): assert len(outputs["pred_logits"].shape) > 2, "Incorrect predicted logits shape" @@ -444,9 +440,7 @@ def get_loss(self, outputs, targets, indices, num_boxes): pass if self.weak_loss: - assert not self.use_separate_loss_for_det_and_trk, ( - "Do not use weak_loss in this case -- set separate loss for detection and tracking queries instead" - ) + assert not self.use_separate_loss_for_det_and_trk, "Do not use weak_loss in this case -- set separate loss for detection and tracking queries instead" # nullify the negative loss for the non-exhaustive classes assert loss_bce.shape[0] == targets["is_exhaustive"].shape[0] @@ -500,9 +494,9 @@ def get_loss(self, outputs, targets, indices, num_boxes): loss_bce = loss_bce.mean() else: assert isinstance(self.pad_n_queries, int) - assert loss_bce.size(1) < self.pad_n_queries, ( - f"The number of predictions is more than the expected total after padding. Got {loss_bce.size(1)} predictions." - ) + assert ( + loss_bce.size(1) < self.pad_n_queries + ), f"The number of predictions is more than the expected total after padding. Got {loss_bce.size(1)} predictions." loss_bce = loss_bce.sum() / (self.pad_n_queries * loss_bce.size(0)) bce_f1 = torchmetrics.functional.f1_score( diff --git a/sam3/train/masks_ops.py b/sam3/train/masks_ops.py index 113a9c4..4e966bd 100644 --- a/sam3/train/masks_ops.py +++ b/sam3/train/masks_ops.py @@ -36,6 +36,23 @@ def instance_masks_to_semantic_masks( return torch.stack([torch.any(masks, dim=0) for masks in masks_per_query], dim=0) +def mask_intersection_vectorized(masks1, masks2): + """ + Vectorized computation of mask intersection using Matrix Multiplication. + + Args: + masks1: tensor of shape (N, H, W) + masks2: tensor of shape (M, H, W) + Returns: + tensor of shape (N, M) + """ + # Cast to float for Tensor Core acceleration via torch.mm + m1_flat = masks1.flatten(1).float() + m2_flat = masks2.flatten(1).float() + intersection = torch.mm(m1_flat, m2_flat.t()) + return intersection.long() + + def mask_intersection(masks1, masks2, block_size=16): """Compute the intersection of two sets of masks, without blowing the memory""" @@ -63,8 +80,7 @@ def mask_iom(masks1, masks2): assert masks1.shape[1:] == masks2.shape[1:] assert masks1.dtype == torch.bool and masks2.dtype == torch.bool - # intersection = (masks1[:, None] * masks2[None]).flatten(-2).sum(-1) - intersection = mask_intersection(masks1, masks2) + intersection = mask_intersection_vectorized(masks1, masks2) area1 = masks1.flatten(-2).sum(-1) area2 = masks2.flatten(-2).sum(-1) min_area = torch.min(area1[:, None], area2[None, :]) @@ -103,9 +119,9 @@ def dilation(mask, kernel_size): assert mask.ndim == 3 kernel_size = int(kernel_size) - assert kernel_size % 2 == 1, ( - f"Dilation expects a odd kernel size, got {kernel_size}" - ) + assert ( + kernel_size % 2 == 1 + ), f"Dilation expects a odd kernel size, got {kernel_size}" if mask.is_cuda: m = mask.unsqueeze(1).to(torch.float16) diff --git a/sam3/train/matcher.py b/sam3/train/matcher.py index 5adc6a4..d3e323e 100644 --- a/sam3/train/matcher.py +++ b/sam3/train/matcher.py @@ -59,9 +59,9 @@ def __init__( self.cost_bbox = cost_bbox self.cost_giou = cost_giou self.norm = nn.Sigmoid() if focal_loss else nn.Softmax(-1) - assert cost_class != 0 or cost_bbox != 0 or cost_giou != 0, ( - "all costs cant be 0" - ) + assert ( + cost_class != 0 or cost_bbox != 0 or cost_giou != 0 + ), "all costs cant be 0" self.focal_loss = focal_loss self.focal_alpha = focal_alpha self.focal_gamma = focal_gamma @@ -196,9 +196,9 @@ def __init__( self.cost_bbox = cost_bbox self.cost_giou = cost_giou self.norm = nn.Sigmoid() - assert cost_class != 0 or cost_bbox != 0 or cost_giou != 0, ( - "all costs cant be 0" - ) + assert ( + cost_class != 0 or cost_bbox != 0 or cost_giou != 0 + ), "all costs cant be 0" @torch.no_grad() def forward(self, outputs, batched_targets, repeats=0, repeat_batch=1): @@ -321,9 +321,9 @@ def __init__( self.alpha = alpha self.gamma = gamma self.stable = stable - assert cost_class != 0 or cost_bbox != 0 or cost_giou != 0, ( - "all costs cant be 0" - ) + assert ( + cost_class != 0 or cost_bbox != 0 or cost_giou != 0 + ), "all costs cant be 0" @torch.no_grad() def forward(self, outputs, batched_targets, repeats=1, repeat_batch=1): @@ -469,9 +469,9 @@ def __init__( self.cost_bbox = cost_bbox self.cost_giou = cost_giou self.norm = nn.Sigmoid() - assert cost_class != 0 or cost_bbox != 0 or cost_giou != 0, ( - "all costs cant be 0" - ) + assert ( + cost_class != 0 or cost_bbox != 0 or cost_giou != 0 + ), "all costs cant be 0" self.focal = focal if focal: self.alpha = alpha diff --git a/sam3/train/optim/optimizer.py b/sam3/train/optim/optimizer.py index 66f305e..d0e7b75 100644 --- a/sam3/train/optim/optimizer.py +++ b/sam3/train/optim/optimizer.py @@ -211,9 +211,9 @@ def unix_module_cls_pattern_to_parameter_names( "match any classes in the model" ) matching_parameters = module_cls_to_param_names[module_cls] - assert len(matching_parameters) > 0, ( - f"module_cls_name {module_cls_name} does not contain any parameters in the model" - ) + assert ( + len(matching_parameters) > 0 + ), f"module_cls_name {module_cls_name} does not contain any parameters in the model" logging.info( f"Matches for module_cls_name [{module_cls_name}]: {matching_parameters} " ) @@ -239,9 +239,9 @@ def unix_param_pattern_to_parameter_names( allowed_parameter_names = [] for param_name in filter_param_names: matching_parameters = set(fnmatch.filter(parameter_names, param_name)) - assert len(matching_parameters) >= 1, ( - f"param_name {param_name} does not match any parameters in the model" - ) + assert ( + len(matching_parameters) >= 1 + ), f"param_name {param_name} does not match any parameters in the model" logging.info(f"Matches for param_name [{param_name}]: {matching_parameters}") allowed_parameter_names.append(matching_parameters) return set.union(*allowed_parameter_names) diff --git a/sam3/train/train.py b/sam3/train/train.py index df6cbe1..b192059 100644 --- a/sam3/train/train.py +++ b/sam3/train/train.py @@ -209,9 +209,9 @@ def main(args) -> None: }, } if "include_nodes" in submitit_conf: - assert len(submitit_conf["include_nodes"]) >= cfg.launcher.num_nodes, ( - "Not enough nodes" - ) + assert ( + len(submitit_conf["include_nodes"]) >= cfg.launcher.num_nodes + ), "Not enough nodes" job_kwargs["slurm_additional_parameters"]["nodelist"] = " ".join( submitit_conf["include_nodes"] ) diff --git a/sam3/train/trainer.py b/sam3/train/trainer.py index cc6c37e..5a37886 100644 --- a/sam3/train/trainer.py +++ b/sam3/train/trainer.py @@ -209,9 +209,9 @@ def __init__( set_seeds(seed_value, self.max_epochs, self.distributed_rank) log_env_variables() - assert is_dist_avail_and_initialized(), ( - "Torch distributed needs to be initialized before calling the trainer." - ) + assert ( + is_dist_avail_and_initialized() + ), "Torch distributed needs to be initialized before calling the trainer." self._setup_components() # Except Optimizer everything is setup here. self._move_to_device() @@ -221,9 +221,9 @@ def __init__( self.time_elapsed_meter = DurationMeter("Time Elapsed", self.device, ":.2f") if self.checkpoint_conf.resume_from is not None: - assert os.path.exists(self.checkpoint_conf.resume_from), ( - f"The 'resume_from' checkpoint {self.checkpoint_conf.resume_from} does not exist!" - ) + assert os.path.exists( + self.checkpoint_conf.resume_from + ), f"The 'resume_from' checkpoint {self.checkpoint_conf.resume_from} does not exist!" dst = os.path.join(self.checkpoint_conf.save_dir, "checkpoint.pt") if self.distributed_rank == 0 and not os.path.exists(dst): # Copy the "resume_from" checkpoint to the checkpoint folder @@ -471,9 +471,9 @@ def _find_loss(self, key: str): return self.loss[key] assert key != "all", "Loss must be specified for key='all'" - assert "default" in self.loss, ( - f"Key {key} not found in losss, and no default provided" - ) + assert ( + "default" in self.loss + ), f"Key {key} not found in losss, and no default provided" return self.loss["default"] def _find_meter(self, phase: str, key: str): @@ -916,12 +916,12 @@ def _run_step( self.optim.zero_grad(set_to_none=True) if self.gradient_accumulation_steps > 1: - assert isinstance(batch, list), ( - f"Expected a list of batches, got {type(batch)}" - ) - assert len(batch) == self.gradient_accumulation_steps, ( - f"Expected {self.gradient_accumulation_steps} batches, got {len(batch)}" - ) + assert isinstance( + batch, list + ), f"Expected a list of batches, got {type(batch)}" + assert ( + len(batch) == self.gradient_accumulation_steps + ), f"Expected {self.gradient_accumulation_steps} batches, got {len(batch)}" accum_steps = len(batch) else: accum_steps = 1 @@ -1033,9 +1033,9 @@ def _reset_meters(self, phases: str) -> None: def _check_val_key_match(self, val_keys, phase): if val_keys is not None: # Check if there are any duplicates - assert len(val_keys) == len(set(val_keys)), ( - f"Duplicate keys in val datasets, keys: {val_keys}" - ) + assert len(val_keys) == len( + set(val_keys) + ), f"Duplicate keys in val datasets, keys: {val_keys}" # Check that the keys match the meter keys if self.meters_conf is not None and phase in self.meters_conf: @@ -1049,9 +1049,9 @@ def _check_val_key_match(self, val_keys, phase): loss_keys = set(self.loss_conf.keys()) - set(["all"]) if "default" not in loss_keys: for k in val_keys: - assert k in loss_keys, ( - f"Error: key {k} is not defined in the losses, and no default is set" - ) + assert ( + k in loss_keys + ), f"Error: key {k} is not defined in the losses, and no default is set" def _setup_components(self): # Get the keys for all the val datasets, if any diff --git a/sam3/train/transforms/basic.py b/sam3/train/transforms/basic.py index 7211389..b08dfae 100644 --- a/sam3/train/transforms/basic.py +++ b/sam3/train/transforms/basic.py @@ -276,9 +276,9 @@ def __call__(self, img: PIL.Image.Image, target: dict): max(0, minY - h + 1), max(maxY - 1, max(0, minY - h + 1)) ) result_img, result_target = crop(img, target, [j, i, h, w]) - assert len(result_target["boxes"]) == init_boxes, ( - f"img_w={img.width}\timg_h={img.height}\tminX={minX}\tminY={minY}\tmaxX={maxX}\tmaxY={maxY}\tminW={minW}\tminH={minH}\tmaxW={maxW}\tmaxH={maxH}\tw={w}\th={h}\ti={i}\tj={j}\tinit_boxes={init_boxes_tensor}\tresults={result_target['boxes']}" - ) + assert ( + len(result_target["boxes"]) == init_boxes + ), f"img_w={img.width}\timg_h={img.height}\tminX={minX}\tminY={minY}\tmaxX={maxX}\tmaxY={maxY}\tminW={minW}\tminH={minH}\tmaxW={maxW}\tmaxH={maxH}\tw={w}\th={h}\ti={i}\tj={j}\tinit_boxes={init_boxes_tensor}\tresults={result_target['boxes']}" return result_img, result_target else: diff --git a/sam3/train/transforms/filter_query_transforms.py b/sam3/train/transforms/filter_query_transforms.py index 3ebd64e..de66c93 100644 --- a/sam3/train/transforms/filter_query_transforms.py +++ b/sam3/train/transforms/filter_query_transforms.py @@ -379,9 +379,9 @@ def __call__(self, datapoint, **kwargs): if len(new_find_queries) == 0: start_with_zero_check = True - assert start_with_zero_check, ( - "Invalid Find queries, they need to start at query_processing_order = 0" - ) + assert ( + start_with_zero_check + ), "Invalid Find queries, they need to start at query_processing_order = 0" datapoint.find_queries = new_find_queries diff --git a/sam3/train/transforms/point_sampling.py b/sam3/train/transforms/point_sampling.py index 521e2f5..d27c6b8 100644 --- a/sam3/train/transforms/point_sampling.py +++ b/sam3/train/transforms/point_sampling.py @@ -249,9 +249,9 @@ def _get_original_box(self, target_object): def _get_target_object(self, datapoint, query): img = datapoint.images[query.image_id] targets = query.object_ids_output - assert len(targets) == 1, ( - "Geometric queries only support a single target object." - ) + assert ( + len(targets) == 1 + ), "Geometric queries only support a single target object." target_idx = targets[0] return img.objects[target_idx] diff --git a/sam3/train/utils/checkpoint_utils.py b/sam3/train/utils/checkpoint_utils.py index 32ca776..465e006 100644 --- a/sam3/train/utils/checkpoint_utils.py +++ b/sam3/train/utils/checkpoint_utils.py @@ -36,9 +36,9 @@ def unix_pattern_to_parameter_names( parameter_names = [] for param_name in constraints: matching_parameters = set(fnmatch.filter(all_parameter_names, param_name)) - assert len(matching_parameters) > 0, ( - f"param_names {param_name} don't match any param in the given names." - ) + assert ( + len(matching_parameters) > 0 + ), f"param_names {param_name} don't match any param in the given names." parameter_names.append(matching_parameters) return set.union(*parameter_names) diff --git a/sam3/train/utils/train_utils.py b/sam3/train/utils/train_utils.py index ca259a3..27e9b93 100644 --- a/sam3/train/utils/train_utils.py +++ b/sam3/train/utils/train_utils.py @@ -82,9 +82,9 @@ def get_machine_local_and_dist_rank(): """ local_rank = int(os.environ.get("LOCAL_RANK", None)) distributed_rank = int(os.environ.get("RANK", None)) - assert local_rank is not None and distributed_rank is not None, ( - "Please the set the RANK and LOCAL_RANK environment variables." - ) + assert ( + local_rank is not None and distributed_rank is not None + ), "Please the set the RANK and LOCAL_RANK environment variables." return local_rank, distributed_rank diff --git a/sam3/visualization_utils.py b/sam3/visualization_utils.py index c007c38..b39d8a4 100644 --- a/sam3/visualization_utils.py +++ b/sam3/visualization_utils.py @@ -158,22 +158,22 @@ def plot_mask(mask, color="r", ax=None): def normalize_bbox(bbox_xywh, img_w, img_h): # Assumes bbox_xywh is in XYWH format if isinstance(bbox_xywh, list): - assert len(bbox_xywh) == 4, ( - "bbox_xywh list must have 4 elements. Batching not support except for torch tensors." - ) + assert ( + len(bbox_xywh) == 4 + ), "bbox_xywh list must have 4 elements. Batching not support except for torch tensors." normalized_bbox = bbox_xywh.copy() normalized_bbox[0] /= img_w normalized_bbox[1] /= img_h normalized_bbox[2] /= img_w normalized_bbox[3] /= img_h else: - assert isinstance(bbox_xywh, torch.Tensor), ( - "Only torch tensors are supported for batching." - ) + assert isinstance( + bbox_xywh, torch.Tensor + ), "Only torch tensors are supported for batching." normalized_bbox = bbox_xywh.clone() - assert normalized_bbox.size(-1) == 4, ( - "bbox_xywh tensor must have last dimension of size 4." - ) + assert ( + normalized_bbox.size(-1) == 4 + ), "bbox_xywh tensor must have last dimension of size 4." normalized_bbox[..., 0] /= img_w normalized_bbox[..., 1] /= img_h normalized_bbox[..., 2] /= img_w @@ -245,9 +245,9 @@ def visualize_formatted_frame_output( num_outputs = len(outputs_list) if titles is None: titles = [f"Set {i + 1}" for i in range(num_outputs)] - assert len(titles) == num_outputs, ( - "length of `titles` should match that of `outputs_list` if not None." - ) + assert ( + len(titles) == num_outputs + ), "length of `titles` should match that of `outputs_list` if not None." _, axes = plt.subplots(1, num_outputs, figsize=figsize) if num_outputs == 1: @@ -703,9 +703,9 @@ def get_all_annotations_for_frame( # Get the frame video_df_current = video_df[video_df.id == video_id] - assert len(video_df_current) == 1, ( - f"Expected 1 video row, got {len(video_df_current)}" - ) + assert ( + len(video_df_current) == 1 + ), f"Expected 1 video row, got {len(video_df_current)}" video_row = video_df_current.iloc[0] file_name = video_row.file_names[frame_idx] file_path = os.path.join( diff --git a/scripts/eval/silver/download_videos.py b/scripts/eval/silver/download_videos.py index d0a1ba8..9b6f873 100644 --- a/scripts/eval/silver/download_videos.py +++ b/scripts/eval/silver/download_videos.py @@ -243,9 +243,9 @@ def download_sav(): def main(): assert len(sys.argv) > 1, "You have to provide the name of the dataset" dataset_name = sys.argv[1] - assert dataset_name in annotation_files, ( - f"The dataset can be one of {list(annotation_files.keys())}" - ) + assert ( + dataset_name in annotation_files + ), f"The dataset can be one of {list(annotation_files.keys())}" if dataset_name == "yt1b": download_youtube() diff --git a/scripts/eval/silver/extract_frames.py b/scripts/eval/silver/extract_frames.py index 5c4285d..6be7f05 100644 --- a/scripts/eval/silver/extract_frames.py +++ b/scripts/eval/silver/extract_frames.py @@ -68,9 +68,9 @@ def process_image(args): def main(): assert len(sys.argv) > 1, "You have to provide the name of the dataset" dataset_name = sys.argv[1] - assert dataset_name in annotation_files, ( - f"The dataset can be one of {list(annotation_files.keys())}" - ) + assert ( + dataset_name in annotation_files + ), f"The dataset can be one of {list(annotation_files.keys())}" all_outputs = [] for file in annotation_files[dataset_name]: with open(os.path.join(config["path_annotations"], file), "r") as f: diff --git a/scripts/eval/veval/saco_yt1b_frame_prep_util.py b/scripts/eval/veval/saco_yt1b_frame_prep_util.py index d4f14ac..49c4f79 100644 --- a/scripts/eval/veval/saco_yt1b_frame_prep_util.py +++ b/scripts/eval/veval/saco_yt1b_frame_prep_util.py @@ -58,9 +58,9 @@ def _get_yt_video_id_map_info(self): df = self.yt1b_start_end_time_df[ self.yt1b_start_end_time_df.saco_yt1b_id == self.saco_yt1b_id ] - assert len(df) == 1, ( - f"Expected exactly 1 row for saco_yt1b_id: {self.saco_yt1b_id}, found {len(df)}" - ) + assert ( + len(df) == 1 + ), f"Expected exactly 1 row for saco_yt1b_id: {self.saco_yt1b_id}, found {len(df)}" id_and_frame_map_row = df.iloc[0] yt_video_id = ( @@ -82,9 +82,9 @@ def _get_yt_video_id_map_info(self): def download_youtube_video(self): video_url = f"https://youtube.com/watch?v={self.yt_video_id}" - assert os.path.exists(self.cookies_file), ( - f"Cookies file '{self.cookies_file}' not found. Must have it to download videos." - ) + assert os.path.exists( + self.cookies_file + ), f"Cookies file '{self.cookies_file}' not found. Must have it to download videos." outtmpl = self.raw_video_path diff --git a/scripts/measure_speed.py b/scripts/measure_speed.py new file mode 100644 index 0000000..bacea1a --- /dev/null +++ b/scripts/measure_speed.py @@ -0,0 +1,304 @@ +""" +SAM3 Speed Test — supports both SAM3 and SAM3.1 (multiplex). + +Generates synthetic video with moving circles, runs text-prompt detection ++ propagation, and measures FPS. Checkpoints are auto-downloaded from +HuggingFace if not provided. + +Usage: + # SAM 3.1 (default, auto-downloads from HuggingFace): + python scripts/measure_speed.py + + # SAM 3 (non-multiplex): + python scripts/measure_speed.py --version sam3 + + # Custom settings: + python scripts/measure_speed.py --num_objects 32 --n_frames 100 --no-compile + python scripts/measure_speed.py --version sam3.1 --compile --num_objects 5 +""" + +import argparse +import getpass +import os +import shutil +import time + +import numpy as np +import torch +from PIL import Image, ImageDraw + + +def max_memory_allocated(): + max_memory_allocated_bytes = torch.cuda.max_memory_allocated() + _, total_memory = torch.cuda.mem_get_info() + max_memory_allocated_percentage = int( + 100 * (max_memory_allocated_bytes / total_memory) + ) + max_memory_allocated_bytes = max_memory_allocated_bytes >> 20 + print( + f"max_memory_allocated_bytes: {max_memory_allocated_bytes}MiB or {max_memory_allocated_percentage}%" + ) + + +def synthesize_video_data( + num_objects: int, + out_dir: str, + radius: int, + speed: int, + width: int, + height: int, + n_frames: int, +): + circle_colors = [ + tuple(np.random.randint(0, 256, size=3).tolist()) for _ in range(num_objects) + ] + + if os.path.exists(out_dir): + shutil.rmtree(out_dir) + os.makedirs(out_dir, exist_ok=True) + + positions = [] + velocities = [] + for _ in range(num_objects): + px = float(np.random.randint(radius, width - radius)) + py = float(np.random.randint(radius, height - radius)) + vx = np.random.choice([-1, 1]) * speed + vy = np.random.choice([-1, 1]) * speed + positions.append([px, py]) + velocities.append([vx, vy]) + + print(f"Generate {n_frames} frames with {num_objects} objects") + for i in range(n_frames): + img = Image.new("RGB", (width, height), (0, 0, 0)) + draw = ImageDraw.Draw(img) + for obj_idx in range(num_objects): + x, y = positions[obj_idx] + rx, ry = round(x), round(y) + draw.ellipse( + [(rx - radius, ry - radius), (rx + radius, ry + radius)], + fill=circle_colors[obj_idx], + ) + vx, vy = velocities[obj_idx] + x += vx + y += vy + positions[obj_idx] = [ + np.clip(x, radius, width - radius), + np.clip(y, radius, height - radius), + ] + if x - radius < 0 or x + radius > width: + vx *= -1 + if y - radius < 0 or y + radius > height: + vy *= -1 + velocities[obj_idx] = [vx, vy] + + img.save(os.path.join(out_dir, f"{i:03d}.jpg")) + + +def profiler_runner(fn, profile_save_dir=None, profile_end_frame=-1, *args, **kwargs): + if profile_save_dir is None: + profile_save_dir = os.path.expanduser("~/traces") + + os.environ["ENABLE_PROFILING"] = "1" + os.environ["PROFILE_SAVE_DIR"] = profile_save_dir + if profile_end_frame >= 0: + os.environ["PROFILE_END_FRAME"] = str(profile_end_frame) + + print(f"Profiling enabled. Traces will be saved to: {profile_save_dir}") + if profile_end_frame >= 0: + print(f"Profiling will stop at frame: {profile_end_frame}") + + try: + result = fn(*args, **kwargs) + finally: + os.environ.pop("ENABLE_PROFILING", None) + os.environ.pop("PROFILE_SAVE_DIR", None) + os.environ.pop("PROFILE_END_FRAME", None) + + return result + + +def main_loop(model_wrapper, session_id, text_prompt): + model_wrapper.handle_request({"type": "reset_session", "session_id": session_id}) + model_wrapper.handle_request( + { + "type": "add_prompt", + "session_id": session_id, + "frame_index": 0, + "text": text_prompt, + } + ) + + t0 = time.perf_counter() + frame_count = 0 + for _response in model_wrapper.handle_stream_request( + {"type": "propagate_in_video", "session_id": session_id} + ): + frame_count += 1 + torch.cuda.synchronize() + t1 = time.perf_counter() + + if frame_count > 0: + return frame_count / (t1 - t0) + return -1 + + +def run_test( + version: str, + profile: bool, + video_dir: str, + num_objects: int, + radius: int, + speed: int, + width: int, + height: int, + n_frames: int, + synthesize_data: bool = True, + profile_save_dir: str = None, + profile_end_frame: int = -1, + do_compile: bool = True, + checkpoint_path: str = None, +) -> float: + torch.autocast(device_type="cuda", dtype=torch.bfloat16).__enter__() + + if synthesize_data: + synthesize_video_data( + num_objects=num_objects, + out_dir=video_dir, + radius=radius, + speed=speed, + width=width, + height=height, + n_frames=n_frames, + ) + + from sam3 import build_sam3_predictor + + print(f"Building {version} model...") + build_kwargs = dict( + version=version, + compile=do_compile, + async_loading_frames=False, + ) + if checkpoint_path: + build_kwargs["checkpoint_path"] = checkpoint_path + if version == "sam3.1": + build_kwargs["warm_up"] = do_compile + build_kwargs["max_num_objects"] = num_objects + + model_wrapper = build_sam3_predictor(**build_kwargs) + + # Initialize session + response = model_wrapper.handle_request( + {"type": "start_session", "resource_path": video_dir} + ) + session_id = response["session_id"] + + print("\nWarm-up round.") + NUM_WARMUP_TRIES = 3 + fps = 0 + for _ in range(NUM_WARMUP_TRIES): + fps = max( + main_loop( + model_wrapper=model_wrapper, session_id=session_id, text_prompt="circle" + ), + fps, + ) + + print("\nProfile round.") + if profile: + profiler_runner( + main_loop, + profile_save_dir=profile_save_dir or os.path.expanduser("~/traces"), + profile_end_frame=profile_end_frame, + model_wrapper=model_wrapper, + session_id=session_id, + text_prompt="circle", + ) + else: + fps = max( + main_loop( + model_wrapper=model_wrapper, session_id=session_id, text_prompt="circle" + ), + fps, + ) + + NUM_TRIES = 10 + for i in range(NUM_TRIES): + torch.cuda.empty_cache() + torch.cuda.reset_peak_memory_stats() + print(f"\nTiming round {i + 1} ") + fps = max( + main_loop( + model_wrapper=model_wrapper, session_id=session_id, text_prompt="circle" + ), + fps, + ) + print(f"Frames per second (FPS): {fps:.2f}") + max_memory_allocated() + + if synthesize_data: + print("\nDeleting temporary video directory.") + shutil.rmtree(video_dir) + + return fps + + +if __name__ == "__main__": + username = getpass.getuser() + os.environ["TORCHINDUCTOR_CACHE_DIR"] = f"/tmp/torchinductor_cache_{username}" + os.environ["USE_PERFLIB"] = "1" + + parser = argparse.ArgumentParser(description="SAM3 Speed Test") + parser.add_argument( + "--version", + type=str, + default="sam3.1", + choices=["sam3", "sam3.1"], + help="Model version (default: sam3.1)", + ) + parser.add_argument( + "--checkpoint", + type=str, + default=None, + help="Path to checkpoint (auto-downloads from HuggingFace if not provided)", + ) + parser.add_argument( + "--video_dir", type=str, default="/tmp/segment-anything-3/synth_video" + ) + parser.add_argument("--num_objects", type=int, default=5) + parser.add_argument("--n_frames", type=int, default=50) + parser.add_argument("--radius", type=int, default=50) + parser.add_argument("--speed", type=int, default=20) + parser.add_argument("--width", type=int, default=1024) + parser.add_argument("--height", type=int, default=1024) + parser.add_argument( + "--no-compile", + action="store_false", + dest="compile", + help="Disable torch.compile", + ) + parser.add_argument("--no-torch-profiling", action="store_false", dest="profile") + parser.add_argument( + "--no-data-synthesis", action="store_false", dest="synthesize_data" + ) + parser.add_argument("--profile-save-dir", type=str, default=None) + parser.add_argument("--profile-end-frame", type=int, default=-1) + + args = parser.parse_args() + + run_test( + version=args.version, + profile=args.profile, + num_objects=args.num_objects, + video_dir=args.video_dir, + radius=args.radius, + speed=args.speed, + width=args.width, + height=args.height, + n_frames=args.n_frames, + synthesize_data=args.synthesize_data, + profile_save_dir=args.profile_save_dir, + profile_end_frame=args.profile_end_frame, + do_compile=args.compile, + checkpoint_path=args.checkpoint, + ) diff --git a/scripts/qualitative_test.py b/scripts/qualitative_test.py new file mode 100644 index 0000000..2b5a881 --- /dev/null +++ b/scripts/qualitative_test.py @@ -0,0 +1,312 @@ +""" +SAM3 Qualitative Test — supports both SAM3 and SAM3.1. + +Tests text prompt detection + propagation on a synthetic video. +Checkpoints are auto-downloaded from HuggingFace. + +Usage: + python scripts/qualitative_test.py # SAM 3.1 default + python scripts/qualitative_test.py --version sam3 # SAM 3 + python scripts/qualitative_test.py --video /path/to/video.mp4 +""" + +import argparse +import getpass +import os +import shutil + +import cv2 +import matplotlib +import numpy as np +import torch +from PIL import Image as PIL_Image, ImageDraw + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from PIL import Image as PIL_Image, ImageDraw + + +OUTPUT_DIR = "/tmp/sam3_qualitative_test" + +MASK_COLORS = [ + (255, 0, 0), + (0, 255, 0), + (0, 0, 255), + (255, 255, 0), + (255, 0, 255), + (0, 255, 255), + (255, 128, 0), + (128, 0, 255), + (0, 128, 255), + (255, 64, 128), + (128, 255, 0), + (64, 128, 255), + (255, 200, 0), + (0, 200, 128), + (200, 0, 128), + (128, 128, 255), + (255, 128, 128), + (128, 255, 128), + (128, 128, 0), + (0, 128, 128), +] + + +def extract_frames(video_path, output_dir): + if os.path.exists(output_dir) and len(os.listdir(output_dir)) > 0: + n = len([f for f in os.listdir(output_dir) if f.endswith(".jpg")]) + print(f"Using existing {n} frames in {output_dir}") + return n + if os.path.exists(output_dir): + shutil.rmtree(output_dir) + os.makedirs(output_dir) + cap = cv2.VideoCapture(video_path) + idx = 0 + while True: + ret, frame = cap.read() + if not ret: + break + cv2.imwrite(os.path.join(output_dir, f"{idx:05d}.jpg"), frame) + idx += 1 + cap.release() + print(f"Extracted {idx} frames to {output_dir}") + return idx + + +def synthesize_video(out_dir, num_objects=5, n_frames=30, width=1024, height=1024): + if os.path.exists(out_dir): + shutil.rmtree(out_dir) + os.makedirs(out_dir) + colors = [ + tuple(np.random.randint(0, 256, size=3).tolist()) for _ in range(num_objects) + ] + positions = [ + [ + float(np.random.randint(80, width - 80)), + float(np.random.randint(80, height - 80)), + ] + for _ in range(num_objects) + ] + velocities = [ + [np.random.choice([-1, 1]) * 15, np.random.choice([-1, 1]) * 15] + for _ in range(num_objects) + ] + for i in range(n_frames): + img = PIL_Image.new("RGB", (width, height), (0, 0, 0)) + draw = ImageDraw.Draw(img) + for j in range(num_objects): + x, y = positions[j] + draw.ellipse([(x - 50, y - 50), (x + 50, y + 50)], fill=colors[j]) + vx, vy = velocities[j] + positions[j] = [ + np.clip(x + vx, 50, width - 50), + np.clip(y + vy, 50, height - 50), + ] + if x < 50 or x > width - 50: + velocities[j][0] *= -1 + if y < 50 or y > height - 50: + velocities[j][1] *= -1 + img.save(os.path.join(out_dir, f"{i:05d}.jpg")) + print(f"Generated {n_frames} synthetic frames with {num_objects} circles") + return n_frames + + +def load_frame(frame_dir, frame_idx): + return cv2.cvtColor( + cv2.imread(os.path.join(frame_dir, f"{frame_idx:05d}.jpg")), + cv2.COLOR_BGR2RGB, + ) + + +def render_overlay(frame_rgb, masks_by_obj, alpha=0.4): + overlay = frame_rgb.copy().astype(np.float32) + for obj_id, mask in sorted(masks_by_obj.items()): + color = MASK_COLORS[obj_id % len(MASK_COLORS)] + mask_bool = mask.astype(bool) + for c in range(3): + overlay[:, :, c] = np.where( + mask_bool, + overlay[:, :, c] * (1 - alpha) + color[c] * alpha, + overlay[:, :, c], + ) + return overlay.astype(np.uint8) + + +def save_overlay(frame_rgb, masks_by_obj, output_path, title=None): + overlay = render_overlay(frame_rgb, masks_by_obj) + fig, ax = plt.subplots(1, 1, figsize=(12, 7), dpi=100) + ax.imshow(overlay) + for obj_id, mask in sorted(masks_by_obj.items()): + mask_bool = mask.astype(bool) + if mask_bool.any(): + ys, xs = np.where(mask_bool) + cx, cy = int(xs.mean()), int(ys.mean()) + color_rgb = MASK_COLORS[obj_id % len(MASK_COLORS)] + facecolor = (color_rgb[0] / 255, color_rgb[1] / 255, color_rgb[2] / 255) + ax.text( + cx, + cy, + str(obj_id), + color="white", + fontsize=10, + ha="center", + va="center", + fontweight="bold", + bbox=dict(boxstyle="round,pad=0.2", facecolor=facecolor, alpha=0.8), + ) + if title: + ax.set_title(title, fontsize=12, fontweight="bold", pad=8) + ax.axis("off") + fig.tight_layout(pad=0) + fig.savefig(output_path, bbox_inches="tight", pad_inches=0) + plt.close(fig) + + +def collect_propagation(model, session_id): + mask_dict = {} + for response in model.handle_stream_request( + {"type": "propagate_in_video", "session_id": session_id} + ): + frame_idx = response.get("frame_index") + if frame_idx is None: + continue + outputs = response.get("outputs", {}) + obj_ids = outputs.get("out_obj_ids", []) + binary_masks = outputs.get("out_binary_masks") + if binary_masks is None: + mask_dict[frame_idx] = {} + continue + if isinstance(obj_ids, torch.Tensor): + obj_ids = obj_ids.cpu().numpy() + if isinstance(binary_masks, torch.Tensor): + binary_masks = binary_masks.cpu().numpy() + masks = {} + for i, oid in enumerate(obj_ids): + m = binary_masks[i] + if m.ndim == 3: + m = m[0] + masks[int(oid)] = m + mask_dict[frame_idx] = masks + torch.cuda.synchronize() + return mask_dict + + +def main(): + parser = argparse.ArgumentParser(description="SAM3 Qualitative Test") + parser.add_argument( + "--version", type=str, default="sam3.1", choices=["sam3", "sam3.1"] + ) + parser.add_argument( + "--video", + type=str, + default=None, + help="Path to video file. If not provided, generates synthetic video.", + ) + parser.add_argument( + "--checkpoint", + type=str, + default=None, + help="Path to checkpoint (auto-downloads from HuggingFace if not provided)", + ) + parser.add_argument( + "--text_prompt", type=str, default="circle", help="Text prompt for detection" + ) + parser.add_argument( + "--n_frames", type=int, default=30, help="Number of frames for synthetic video" + ) + args = parser.parse_args() + + username = getpass.getuser() + os.environ["TORCHINDUCTOR_CACHE_DIR"] = f"/tmp/torchinductor_cache_{username}" + os.environ["USE_PERFLIB"] = "1" + torch.autocast(device_type="cuda", dtype=torch.bfloat16).__enter__() + + # Prepare video frames + frame_dir = "/tmp/sam3_qualitative_frames" + if args.video: + n_frames = extract_frames(args.video, frame_dir) + else: + n_frames = synthesize_video(frame_dir, n_frames=args.n_frames) + + img = load_frame(frame_dir, 0) + img_h, img_w = img.shape[:2] + print(f"Video: {img_w}x{img_h}, {n_frames} frames") + + # Build model + from sam3 import build_sam3_predictor + + print(f"\nBuilding {args.version} model...") + build_kwargs = dict(version=args.version, compile=False, async_loading_frames=False) + if args.checkpoint: + build_kwargs["checkpoint_path"] = args.checkpoint + model = build_sam3_predictor(**build_kwargs) + + # Start session + response = model.handle_request( + {"type": "start_session", "resource_path": frame_dir} + ) + session_id = response["session_id"] + print(f"Session: {session_id}") + + # Test: text prompt -> propagate + out_dir = os.path.join(OUTPUT_DIR, f"{args.version}_text_{args.text_prompt}") + if os.path.exists(out_dir): + shutil.rmtree(out_dir) + os.makedirs(out_dir) + + print(f"\nTest: text prompt '{args.text_prompt}' -> propagate") + model.handle_request( + { + "type": "add_prompt", + "session_id": session_id, + "frame_index": 0, + "text": args.text_prompt, + } + ) + + mask_dict = collect_propagation(model, session_id) + print(f"Propagated through {len(mask_dict)} frames") + + # Save overlays + saved = 0 + for frame_idx in sorted(mask_dict.keys()): + if frame_idx % 5 != 0: + continue + masks = mask_dict[frame_idx] + if not masks: + continue + frame_rgb = load_frame(frame_dir, frame_idx) + save_overlay( + frame_rgb, + masks, + os.path.join(out_dir, f"frame_{frame_idx:05d}.png"), + title=f"{args.version} | frame {frame_idx} | {len(masks)} objects", + ) + saved += 1 + + # Print results + frame0 = mask_dict.get(0, {}) + print(f"\nDetected {len(frame0)} objects on frame 0:") + for obj_id, mask in sorted(frame0.items()): + mask_bool = mask.astype(bool) + n_pixels = int(mask_bool.sum()) + if mask_bool.any(): + ys, xs = np.where(mask_bool) + print( + f" obj {obj_id}: centroid ({int(xs.mean())}, {int(ys.mean())}), {n_pixels} pixels" + ) + + print(f"\nSaved {saved} overlay images to {out_dir}") + print( + "QUALITATIVE TEST PASSED" + if len(frame0) > 0 + else "WARNING: No objects detected!" + ) + + # Cleanup + if not args.video: + shutil.rmtree(frame_dir) + + +if __name__ == "__main__": + main() From 5a3143c652ac41c28b788a419b40ec424d0f6aab Mon Sep 17 00:00:00 2001 From: Joseph Greer Date: Fri, 27 Mar 2026 12:40:51 -0700 Subject: [PATCH 18/31] Apply linter Summary: Applies lint Reviewed By: arpitkalla Differential Revision: D98515496 fbshipit-source-id: 967c9bf19ef7e1e25906204075c37c8461ed2a0e --- sam3/agent/agent_core.py | 6 +- sam3/agent/helpers/boxes.py | 20 +- sam3/agent/helpers/masks.py | 8 +- sam3/agent/helpers/rotated_boxes.py | 6 +- sam3/agent/helpers/visualizer.py | 6 +- sam3/eval/cgf1_eval.py | 36 +-- sam3/eval/coco_eval.py | 6 +- sam3/eval/coco_writer.py | 6 +- sam3/eval/demo_eval.py | 24 +- sam3/eval/postprocessors.py | 16 +- sam3/eval/ytvis_coco_wrapper.py | 6 +- sam3/eval/ytvis_eval.py | 34 ++- sam3/logger.py | 6 +- sam3/model/data_misc.py | 6 +- sam3/model/decoder.py | 49 ++-- sam3/model/encoder.py | 18 +- sam3/model/geometry_encoders.py | 211 ++++++++---------- sam3/model/maskformer_segmentation.py | 4 +- sam3/model/memory.py | 6 +- sam3/model/model_misc.py | 132 ++++++----- sam3/model/multiplex_mask_decoder.py | 54 ++--- sam3/model/multiplex_utils.py | 60 ++--- sam3/model/sam1_task_predictor.py | 30 +-- sam3/model/sam3_image.py | 6 +- sam3/model/sam3_image_processor.py | 6 +- sam3/model/sam3_multiplex_base.py | 40 ++-- sam3/model/sam3_multiplex_detector.py | 6 +- sam3/model/sam3_multiplex_tracking.py | 50 ++--- sam3/model/sam3_video_base.py | 40 ++-- sam3/model/sam3_video_inference.py | 46 ++-- sam3/model/text_encoder_ve.py | 6 +- sam3/model/video_tracking_multiplex.py | 63 +++--- sam3/model/vitdet.py | 6 +- sam3/model/vl_combiner.py | 12 +- sam3/perflib/connected_components.py | 12 +- sam3/perflib/triton/connected_components.py | 12 +- sam3/sam/mask_decoder.py | 6 +- sam3/sam/transformer.py | 6 +- sam3/train/data/collator.py | 6 +- sam3/train/data/sam3_image_dataset.py | 12 +- sam3/train/data/sam3_video_dataset.py | 7 +- sam3/train/loss/loss_fns.py | 18 +- sam3/train/masks_ops.py | 6 +- sam3/train/matcher.py | 24 +- sam3/train/optim/optimizer.py | 12 +- sam3/train/train.py | 6 +- sam3/train/transforms/basic.py | 6 +- .../transforms/filter_query_transforms.py | 6 +- sam3/train/transforms/point_sampling.py | 6 +- sam3/train/utils/checkpoint_utils.py | 6 +- sam3/train/utils/train_utils.py | 6 +- sam3/visualization_utils.py | 30 +-- scripts/eval/silver/extract_frames.py | 6 +- .../eval/veval/saco_yt1b_frame_prep_util.py | 12 +- 54 files changed, 618 insertions(+), 618 deletions(-) diff --git a/sam3/agent/agent_core.py b/sam3/agent/agent_core.py index 27e5120..8b4fc8a 100644 --- a/sam3/agent/agent_core.py +++ b/sam3/agent/agent_core.py @@ -296,9 +296,9 @@ def agent_inference( assert LATEST_SAM3_TEXT_PROMPT != "" # Make sure that the last message is a image - assert ( - messages[-1]["content"][1]["type"] == "image" - ), "Second content element should be an image" + assert messages[-1]["content"][1]["type"] == "image", ( + "Second content element should be an image" + ) messages.pop() # Remove the last user message # Add simplified replacement message simplified_message = { diff --git a/sam3/agent/helpers/boxes.py b/sam3/agent/helpers/boxes.py index a32e520..df44769 100755 --- a/sam3/agent/helpers/boxes.py +++ b/sam3/agent/helpers/boxes.py @@ -84,9 +84,9 @@ def convert( ], "Relative mode not yet supported!" if from_mode == BoxMode.XYWHA_ABS and to_mode == BoxMode.XYXY_ABS: - assert ( - arr.shape[-1] == 5 - ), "The last dimension of input shape must be 5 for XYWHA format" + assert arr.shape[-1] == 5, ( + "The last dimension of input shape must be 5 for XYWHA format" + ) original_dtype = arr.dtype arr = arr.double() @@ -244,9 +244,9 @@ def __getitem__(self, item) -> "Boxes": if isinstance(item, int): return Boxes(self.tensor[item].view(1, -1)) b = self.tensor[item] - assert ( - b.dim() == 2 - ), "Indexing on Boxes with {} failed to return a matrix!".format(item) + assert b.dim() == 2, ( + "Indexing on Boxes with {} failed to return a matrix!".format(item) + ) return Boxes(b) def __len__(self) -> int: @@ -424,10 +424,10 @@ def matched_pairwise_iou(boxes1: Boxes, boxes2: Boxes) -> torch.Tensor: Returns: Tensor: iou, sized [N]. """ - assert len(boxes1) == len( - boxes2 - ), "boxlists should have the samenumber of entries, got {}, {}".format( - len(boxes1), len(boxes2) + assert len(boxes1) == len(boxes2), ( + "boxlists should have the samenumber of entries, got {}, {}".format( + len(boxes1), len(boxes2) + ) ) area1 = boxes1.area() # [N] area2 = boxes2.area() # [N] diff --git a/sam3/agent/helpers/masks.py b/sam3/agent/helpers/masks.py index ba10fc8..a303f6e 100755 --- a/sam3/agent/helpers/masks.py +++ b/sam3/agent/helpers/masks.py @@ -141,10 +141,10 @@ def __getitem__(self, item: Union[int, slice, torch.BoolTensor]) -> "BitMasks": if isinstance(item, int): return BitMasks(self.tensor[item].unsqueeze(0)) m = self.tensor[item] - assert ( - m.dim() == 3 - ), "Indexing on BitMasks with {} returns a tensor with shape {}!".format( - item, m.shape + assert m.dim() == 3, ( + "Indexing on BitMasks with {} returns a tensor with shape {}!".format( + item, m.shape + ) ) return BitMasks(m) diff --git a/sam3/agent/helpers/rotated_boxes.py b/sam3/agent/helpers/rotated_boxes.py index cd39af8..0017335 100755 --- a/sam3/agent/helpers/rotated_boxes.py +++ b/sam3/agent/helpers/rotated_boxes.py @@ -363,9 +363,9 @@ def __getitem__(self, item) -> "RotatedBoxes": if isinstance(item, int): return RotatedBoxes(self.tensor[item].view(1, -1)) b = self.tensor[item] - assert ( - b.dim() == 2 - ), "Indexing on RotatedBoxes with {} failed to return a matrix!".format(item) + assert b.dim() == 2, ( + "Indexing on RotatedBoxes with {} failed to return a matrix!".format(item) + ) return RotatedBoxes(b) def __len__(self) -> int: diff --git a/sam3/agent/helpers/visualizer.py b/sam3/agent/helpers/visualizer.py index bab3dff..2050652 100755 --- a/sam3/agent/helpers/visualizer.py +++ b/sam3/agent/helpers/visualizer.py @@ -221,9 +221,9 @@ def non_empty_mask(self): empty_ids.append(id) if len(empty_ids) == 0: return np.zeros(self._seg.shape, dtype=np.uint8) - assert ( - len(empty_ids) == 1 - ), ">1 ids corresponds to no labels. This is currently not supported" + assert len(empty_ids) == 1, ( + ">1 ids corresponds to no labels. This is currently not supported" + ) return (self._seg != empty_ids[0]).numpy().astype(np.bool) def semantic_masks(self): diff --git a/sam3/eval/cgf1_eval.py b/sam3/eval/cgf1_eval.py index 71fe2ea..cbc91c1 100644 --- a/sam3/eval/cgf1_eval.py +++ b/sam3/eval/cgf1_eval.py @@ -126,9 +126,9 @@ def loadRes(self, resFile): # MODIFICATION: faster and cached subset check if not hasattr(self, "img_id_set"): self.img_id_set = set(self.getImgIds()) - assert set(annsImgIds).issubset( - self.img_id_set - ), "Results do not correspond to current coco set" + assert set(annsImgIds).issubset(self.img_id_set), ( + "Results do not correspond to current coco set" + ) # END MODIFICATION if "caption" in anns[0]: imgIds = set([img["id"] for img in res.dataset["images"]]) & set( @@ -301,9 +301,9 @@ def evaluateImg(self, imgId, catId, aRng, maxDet): TP = (match_scores >= thresh).sum() FP = len(dt) - TP FN = len(gt) - TP - assert ( - FP >= 0 and FN >= 0 - ), f"FP: {FP}, FN: {FN}, TP: {TP}, match_scores: {match_scores}, len(dt): {len(dt)}, len(gt): {len(gt)}, ious: {ious}" + assert FP >= 0 and FN >= 0, ( + f"FP: {FP}, FN: {FN}, TP: {TP}, match_scores: {match_scores}, len(dt): {len(dt)}, len(gt): {len(gt)}, ious: {ious}" + ) TPs.append(TP) FPs.append(FP) FNs.append(FN) @@ -599,9 +599,9 @@ def evaluate(self, pred_file: str): """ assert len(self.coco_gts) > 0, "No ground truth provided for evaluation." - assert len(self.coco_gts) == len( - self.coco_evals - ), "Mismatch in number of ground truths and evaluators." + assert len(self.coco_gts) == len(self.coco_evals), ( + "Mismatch in number of ground truths and evaluators." + ) if self.verbose: print(f"Loading predictions from {pred_file}") @@ -668,17 +668,17 @@ def _select_best_scoring(scorings): if len(scorings) == 1: return scorings[0] - assert ( - scorings[0].ndim == 3 - ), f"Expecting results in [numCats, numAreas, numImgs] format, got {scorings[0].shape}" - assert ( - scorings[0].shape[0] == 1 - ), f"Expecting a single category, got {scorings[0].shape[0]}" + assert scorings[0].ndim == 3, ( + f"Expecting results in [numCats, numAreas, numImgs] format, got {scorings[0].shape}" + ) + assert scorings[0].shape[0] == 1, ( + f"Expecting a single category, got {scorings[0].shape[0]}" + ) for scoring in scorings: - assert ( - scoring.shape == scorings[0].shape - ), f"Shape mismatch: {scoring.shape}, {scorings[0].shape}" + assert scoring.shape == scorings[0].shape, ( + f"Shape mismatch: {scoring.shape}, {scorings[0].shape}" + ) selected_imgs = [] for img_id in range(scorings[0].shape[-1]): diff --git a/sam3/eval/coco_eval.py b/sam3/eval/coco_eval.py index 3716885..7eee615 100644 --- a/sam3/eval/coco_eval.py +++ b/sam3/eval/coco_eval.py @@ -751,9 +751,9 @@ def loadRes(self, resFile): anns = resFile assert type(anns) == list, "results in not an array of objects" annsImgIds = [ann["image_id"] for ann in anns] - assert set(annsImgIds) == ( - set(annsImgIds) & set(self.getImgIds()) - ), "Results do not correspond to current coco set" + assert set(annsImgIds) == (set(annsImgIds) & set(self.getImgIds())), ( + "Results do not correspond to current coco set" + ) if "caption" in anns[0]: imgIds = set([img["id"] for img in res.dataset["images"]]) & set( [ann["image_id"] for ann in anns] diff --git a/sam3/eval/coco_writer.py b/sam3/eval/coco_writer.py index 54f9307..f49fcf2 100644 --- a/sam3/eval/coco_writer.py +++ b/sam3/eval/coco_writer.py @@ -83,9 +83,9 @@ def __init__( self.merge_predictions = merge_predictions self.pred_file_evaluators = pred_file_evaluators if self.pred_file_evaluators is not None: - assert ( - merge_predictions - ), "merge_predictions must be True if pred_file_evaluators are provided" + assert merge_predictions, ( + "merge_predictions must be True if pred_file_evaluators are provided" + ) assert self.dump_dir is not None, "dump_dir must be provided" if is_main_process(): diff --git a/sam3/eval/demo_eval.py b/sam3/eval/demo_eval.py index 353b86a..6ac7063 100644 --- a/sam3/eval/demo_eval.py +++ b/sam3/eval/demo_eval.py @@ -154,9 +154,9 @@ def evaluateImg(self, imgId, catId, aRng, maxDet): TP = (match_scores >= thresh).sum() FP = len(dt) - TP FN = len(gt) - TP - assert ( - FP >= 0 and FN >= 0 - ), f"FP: {FP}, FN: {FN}, TP: {TP}, match_scores: {match_scores}, len(dt): {len(dt)}, len(gt): {len(gt)}, ious: {ious}" + assert FP >= 0 and FN >= 0, ( + f"FP: {FP}, FN: {FN}, TP: {TP}, match_scores: {match_scores}, len(dt): {len(dt)}, len(gt): {len(gt)}, ious: {ious}" + ) TPs.append(TP) FPs.append(FP) FNs.append(FN) @@ -526,17 +526,17 @@ def select_best_scoring(self, scorings): if len(scorings) == 1: return scorings[0] - assert ( - scorings[0].ndim == 3 - ), f"Expecting results in [numCats, numAreas, numImgs] format, got {scorings[0].shape}" - assert ( - scorings[0].shape[0] == 1 - ), f"Expecting a single category, got {scorings[0].shape[0]}" + assert scorings[0].ndim == 3, ( + f"Expecting results in [numCats, numAreas, numImgs] format, got {scorings[0].shape}" + ) + assert scorings[0].shape[0] == 1, ( + f"Expecting a single category, got {scorings[0].shape[0]}" + ) for scoring in scorings: - assert ( - scoring.shape == scorings[0].shape - ), f"Shape mismatch: {scoring.shape}, {scorings[0].shape}" + assert scoring.shape == scorings[0].shape, ( + f"Shape mismatch: {scoring.shape}, {scorings[0].shape}" + ) selected_imgs = [] for img_id in range(scorings[0].shape[-1]): diff --git a/sam3/eval/postprocessors.py b/sam3/eval/postprocessors.py index 7bd26a9..44eb103 100644 --- a/sam3/eval/postprocessors.py +++ b/sam3/eval/postprocessors.py @@ -83,9 +83,9 @@ def forward( ret_tensordict: Experimental argument. If true, return a tensordict.TensorDict instead of a list of dictionaries for easier manipulation. """ if ret_tensordict: - assert ( - consistent is True - ), "We don't support returning TensorDict if the outputs have different shapes" # NOTE: It's possible but we don't support it. + assert consistent is True, ( + "We don't support returning TensorDict if the outputs have different shapes" + ) # NOTE: It's possible but we don't support it. assert self.detection_threshold <= 0.0, "TODO: implement?" try: from tensordict import TensorDict @@ -118,7 +118,9 @@ def forward( if boxes is None: assert out_masks is not None - assert not ret_tensordict, "We don't support returning TensorDict if the output does not contain boxes" + assert not ret_tensordict, ( + "We don't support returning TensorDict if the output does not contain boxes" + ) B = len(out_masks) boxes = [None] * B scores = [None] * B @@ -418,9 +420,9 @@ def process_results( if video_id == -1: video_id = unique_vid_id.item() else: - assert ( - video_id == unique_vid_id.item() - ), "We can only postprocess one video per datapoint" + assert video_id == unique_vid_id.item(), ( + "We can only postprocess one video per datapoint" + ) # keeping track of which objects appear in the current frame obj_ids_per_frame = frame_outs["pred_object_ids"] assert obj_ids_per_frame.size(-1) == frame_outs["pred_logits"].size(-2) diff --git a/sam3/eval/ytvis_coco_wrapper.py b/sam3/eval/ytvis_coco_wrapper.py index 5412d69..25feda4 100644 --- a/sam3/eval/ytvis_coco_wrapper.py +++ b/sam3/eval/ytvis_coco_wrapper.py @@ -95,9 +95,9 @@ def loadRes(self, resFile): anns = resFile assert type(anns) == list, "results is not an array of objects" annsImgIds = [ann["image_id"] for ann in anns] - assert set(annsImgIds) == ( - set(annsImgIds) & set(self.getImgIds()) - ), "Results do not correspond to current coco set" + assert set(annsImgIds) == (set(annsImgIds) & set(self.getImgIds())), ( + "Results do not correspond to current coco set" + ) if "bboxes" in anns[0] and not anns[0]["bboxes"] == []: res.dataset["categories"] = copy.deepcopy(self.dataset["categories"]) for id, ann in enumerate(anns): diff --git a/sam3/eval/ytvis_eval.py b/sam3/eval/ytvis_eval.py index 2ff2309..0754b62 100644 --- a/sam3/eval/ytvis_eval.py +++ b/sam3/eval/ytvis_eval.py @@ -109,9 +109,7 @@ def iou_tracklets(preds, gts): ) # Num preds x Num GTS x Num frames inter = inter.sum(-1) union = union.sum(-1) - assert ( - union > 0 - ).all(), ( + assert (union > 0).all(), ( "There exists a tracklet with zero GTs across time. This is suspicious" ) return inter / union @@ -136,9 +134,9 @@ def iou_masklets(preds, gts): iou = inter / union assert iou >= 0 and iou <= 1, "Encountered an error in IoU computation" else: - assert np.isclose(inter, 0) and np.isclose( - union, 0 - ), "Encountered an error in IoU computation" + assert np.isclose(inter, 0) and np.isclose(union, 0), ( + "Encountered an error in IoU computation" + ) iou = 1 return iou @@ -206,16 +204,16 @@ def prepare(self, predictions): if len(prediction) == 0: continue for k in ["boxes", "scores", "labels"]: - assert ( - k in prediction - ), f"Expected predictions to have `{k}` key, available keys are {prediction.keys()}" + assert k in prediction, ( + f"Expected predictions to have `{k}` key, available keys are {prediction.keys()}" + ) if self.save_per_frame_scores: - assert ( - "per_frame_scores" in prediction - ), f"Expected predictions to have `per_frame_scores` key, available keys are {prediction.keys()}" - assert xor( - "masks" in prediction, "masks_rle" in prediction - ), f"Expected predictions to have either `masks` key or `masks_rle` key, available keys are {prediction.keys()}" + assert "per_frame_scores" in prediction, ( + f"Expected predictions to have `per_frame_scores` key, available keys are {prediction.keys()}" + ) + assert xor("masks" in prediction, "masks_rle" in prediction), ( + f"Expected predictions to have either `masks` key or `masks_rle` key, available keys are {prediction.keys()}" + ) boxes = prediction["boxes"] boxes = convert_to_xywh(boxes).tolist() @@ -223,9 +221,9 @@ def prepare(self, predictions): labels = prediction["labels"].tolist() if "masks" in prediction: masks = prediction["masks"].squeeze(2) - assert ( - masks.ndim == 4 - ), "Expected masks to be of shape(N_preds,T_frames,H,W)" + assert masks.ndim == 4, ( + "Expected masks to be of shape(N_preds,T_frames,H,W)" + ) areas = [mask.flatten(1).sum(1).tolist() for mask in masks] rles = [rle_encode(masklet) for masklet in masks] diff --git a/sam3/logger.py b/sam3/logger.py index 35dcc0d..2ce9c09 100644 --- a/sam3/logger.py +++ b/sam3/logger.py @@ -42,9 +42,9 @@ def get_logger(name, level=logging.INFO): """A command line logger.""" if "LOG_LEVEL" in os.environ: level = os.environ["LOG_LEVEL"].upper() - assert ( - level in LOG_LEVELS - ), f"Invalid LOG_LEVEL: {level}, must be one of {list(LOG_LEVELS.keys())}" + assert level in LOG_LEVELS, ( + f"Invalid LOG_LEVEL: {level}, must be one of {list(LOG_LEVELS.keys())}" + ) level = LOG_LEVELS[level] logger = logging.getLogger(name) logger.setLevel(level) diff --git a/sam3/model/data_misc.py b/sam3/model/data_misc.py index cc6a7bb..bd1ed30 100644 --- a/sam3/model/data_misc.py +++ b/sam3/model/data_misc.py @@ -75,9 +75,9 @@ def interpolate( input, size, scale_factor, mode, align_corners ) - assert ( - input.shape[0] != 0 or input.shape[1] != 0 - ), "At least one of the two first dimensions must be non zero" + assert input.shape[0] != 0 or input.shape[1] != 0, ( + "At least one of the two first dimensions must be non zero" + ) if input.shape[1] == 0: # Pytorch doesn't support null dimension on the channel dimension, so we transpose to fake a null batch dim diff --git a/sam3/model/decoder.py b/sam3/model/decoder.py index 098e2a3..b90e55f 100644 --- a/sam3/model/decoder.py +++ b/sam3/model/decoder.py @@ -444,9 +444,9 @@ def forward( - valid_ratios/spatial_shapes: bs, nlevel, 2 """ if memory_mask is not None: - assert ( - self.boxRPB == "none" - ), "inputting a memory_mask in the presence of boxRPB is unexpected/not implemented" + assert self.boxRPB == "none", ( + "inputting a memory_mask in the presence of boxRPB is unexpected/not implemented" + ) apply_dac = apply_dac if apply_dac is not None else self.dac if apply_dac: @@ -516,18 +516,18 @@ def forward( query_pos = self.ref_point_head(query_sine_embed) # nq, bs, d_model if self.boxRPB != "none" and reference_boxes is not None: - assert ( - spatial_shapes.shape[0] == 1 - ), "only single scale support implemented" + assert spatial_shapes.shape[0] == 1, ( + "only single scale support implemented" + ) memory_mask = self._get_rpb_matrix( reference_boxes, (spatial_shapes[0, 0], spatial_shapes[0, 1]), ) memory_mask = memory_mask.flatten(0, 1) # (bs*n_heads, nq, H*W) if self.training: - assert ( - self.use_act_checkpoint - ), "Activation checkpointing not enabled in the decoder" + assert self.use_act_checkpoint, ( + "Activation checkpointing not enabled in the decoder" + ) output, presence_out = activation_ckpt_wrapper(layer)( tgt=output, tgt_query_pos=query_pos, @@ -676,9 +676,9 @@ def forward( src_pos[0], ) - assert ( - src.shape[1] == prompt.shape[1] - ), "Batch size must be the same for src and prompt" + assert src.shape[1] == prompt.shape[1], ( + "Batch size must be the same for src and prompt" + ) output = src @@ -1291,12 +1291,12 @@ def forward( memory_pos: Optional[Tensor] = None, # pos_enc for cross-attention inputs num_obj_ptr_tokens: int = 0, # number of object pointer *tokens* ): - assert ( - src.shape[1] == memory.shape[1] - ), "Batch size must be the same for src and memory" - assert ( - image.shape[1] == memory_image.shape[1] - ), "Batch size must be the same for image and memory_image" + assert src.shape[1] == memory.shape[1], ( + "Batch size must be the same for src and memory" + ) + assert image.shape[1] == memory_image.shape[1], ( + "Batch size must be the same for image and memory_image" + ) output = src @@ -1315,9 +1315,9 @@ def forward( if memory_image.shape[1] != memory.shape[1]: # Pad memory_image with zeros, to accodmate object pointers - assert ( - (memory.shape[1] - memory_image.shape[1]) == num_obj_ptr_tokens - ), f"{memory.shape[1]} - {memory_image.shape[1]} != {num_obj_ptr_tokens}" + assert (memory.shape[1] - memory_image.shape[1]) == num_obj_ptr_tokens, ( + f"{memory.shape[1]} - {memory_image.shape[1]} != {num_obj_ptr_tokens}" + ) memory_image = torch.cat( [ memory_image, @@ -1332,9 +1332,10 @@ def forward( ) if memory_image_pos is not None: assert ( - (memory_pos.shape[1] - memory_image_pos.shape[1]) - == num_obj_ptr_tokens - ), f"{memory_pos.shape[1]} - {memory_image_pos.shape[1]} != {num_obj_ptr_tokens}" + memory_pos.shape[1] - memory_image_pos.shape[1] + ) == num_obj_ptr_tokens, ( + f"{memory_pos.shape[1]} - {memory_image_pos.shape[1]} != {num_obj_ptr_tokens}" + ) # tpos is the same in the batch anyway; note that memory_image always has a batch size of 1 memory_image_pos = torch.cat( [ diff --git a/sam3/model/encoder.py b/sam3/model/encoder.py index d825df4..3fc9406 100644 --- a/sam3/model/encoder.py +++ b/sam3/model/encoder.py @@ -322,9 +322,9 @@ def get_reference_points(spatial_shapes, valid_ratios, device): return reference_points def _prepare_multilevel_features(self, srcs, masks, pos_embeds): - assert ( - len(srcs) == self.num_feature_levels - ), "mismatch between expected and received # of feature levels" + assert len(srcs) == self.num_feature_levels, ( + "mismatch between expected and received # of feature levels" + ) src_flatten = [] mask_flatten = [] @@ -406,9 +406,9 @@ def forward( - spatial_shapes: Spatial dimensions of each feature level - valid_ratios: Valid ratios for each feature level """ - assert ( - len(src) == self.num_feature_levels - ), "must be equal to num_feature_levels" + assert len(src) == self.num_feature_levels, ( + "must be equal to num_feature_levels" + ) if src_key_padding_masks is not None: assert len(src_key_padding_masks) == self.num_feature_levels if pos is not None: @@ -538,9 +538,9 @@ def forward( else None ) else: - assert all( - x.dim == 4 for x in src - ), "expected list of (bs, c, h, w) tensors" + assert all(x.dim == 4 for x in src), ( + "expected list of (bs, c, h, w) tensors" + ) if self.add_pooled_text_to_img_feat: # Fusion: Add mean pooled text to image features diff --git a/sam3/model/geometry_encoders.py b/sam3/model/geometry_encoders.py index 35a4276..d60ee54 100644 --- a/sam3/model/geometry_encoders.py +++ b/sam3/model/geometry_encoders.py @@ -147,54 +147,42 @@ def __init__( ) # Dimension checks - assert ( - box_embeddings is not None - and list(box_embeddings.shape[:2]) - == [ - box_seq_len, - bs, - ] - ), f"Wrong dimension for box embeddings. Expected [{box_seq_len}, {bs}, *] got {box_embeddings.shape}" - assert ( - box_mask is not None - and list(box_mask.shape) - == [ - bs, - box_seq_len, - ] - ), f"Wrong dimension for box mask. Expected [{bs}, {box_seq_len}] got {box_mask.shape}" - assert ( - point_embeddings is not None - and list(point_embeddings.shape[:2]) - == [ - point_seq_len, - bs, - ] - ), f"Wrong dimension for point embeddings. Expected [{point_seq_len}, {bs}, *] got {point_embeddings.shape}" - assert ( - point_mask is not None - and list(point_mask.shape) - == [ - bs, - point_seq_len, - ] - ), f"Wrong dimension for point mask. Expected [{bs}, {point_seq_len}] got {point_mask.shape}" - assert ( - box_labels is not None - and list(box_labels.shape) - == [ - box_seq_len, - bs, - ] - ), f"Wrong dimension for box labels. Expected [{box_seq_len}, {bs}] got {box_labels.shape}" - assert ( - point_labels is not None - and list(point_labels.shape) - == [ - point_seq_len, - bs, - ] - ), f"Wrong dimension for point labels. Expected [{point_seq_len}, {bs}] got {point_labels.shape}" + assert box_embeddings is not None and list(box_embeddings.shape[:2]) == [ + box_seq_len, + bs, + ], ( + f"Wrong dimension for box embeddings. Expected [{box_seq_len}, {bs}, *] got {box_embeddings.shape}" + ) + assert box_mask is not None and list(box_mask.shape) == [ + bs, + box_seq_len, + ], ( + f"Wrong dimension for box mask. Expected [{bs}, {box_seq_len}] got {box_mask.shape}" + ) + assert point_embeddings is not None and list(point_embeddings.shape[:2]) == [ + point_seq_len, + bs, + ], ( + f"Wrong dimension for point embeddings. Expected [{point_seq_len}, {bs}, *] got {point_embeddings.shape}" + ) + assert point_mask is not None and list(point_mask.shape) == [ + bs, + point_seq_len, + ], ( + f"Wrong dimension for point mask. Expected [{bs}, {point_seq_len}] got {point_mask.shape}" + ) + assert box_labels is not None and list(box_labels.shape) == [ + box_seq_len, + bs, + ], ( + f"Wrong dimension for box labels. Expected [{box_seq_len}, {bs}] got {box_labels.shape}" + ) + assert point_labels is not None and list(point_labels.shape) == [ + point_seq_len, + bs, + ], ( + f"Wrong dimension for point labels. Expected [{point_seq_len}, {bs}] got {point_labels.shape}" + ) assert ( # Allowed to be None, we leave it to the encoder to check for validity before encoding. mask_embeddings is None @@ -203,41 +191,41 @@ def __init__( mask_seq_len, bs, ] - ), f"Wrong dimension for mask embeddings. Expected [{mask_seq_len}, {bs}, *] got {mask_embeddings.shape}" - assert ( - mask_mask is None - or list(mask_mask.shape) - == [ - bs, - mask_seq_len, - ] - ), f"Wrong dimension for mask attn. mask. Expected [{bs}, {mask_seq_len}] got {mask_mask.shape}" + ), ( + f"Wrong dimension for mask embeddings. Expected [{mask_seq_len}, {bs}, *] got {mask_embeddings.shape}" + ) + assert mask_mask is None or list(mask_mask.shape) == [ + bs, + mask_seq_len, + ], ( + f"Wrong dimension for mask attn. mask. Expected [{bs}, {mask_seq_len}] got {mask_mask.shape}" + ) # Device checks - assert ( - box_embeddings is not None and box_embeddings.device == device - ), f"Expected box embeddings to be on device {device}, got {box_embeddings.device}" - assert ( - box_mask is not None and box_mask.device == device - ), f"Expected box mask to be on device {device}, got {box_mask.device}" - assert ( - box_labels is not None and box_labels.device == device - ), f"Expected box labels to be on device {device}, got {box_labels.device}" - assert ( - point_embeddings is not None and point_embeddings.device == device - ), f"Expected point embeddings to be on device {device}, got {point_embeddings.device}" - assert ( - point_mask is not None and point_mask.device == device - ), f"Expected point mask to be on device {device}, got {point_mask.device}" - assert ( - point_labels is not None and point_labels.device == device - ), f"Expected point labels to be on device {device}, got {point_labels.device}" - assert ( - mask_embeddings is None or mask_embeddings.device == device - ), f"Expected mask embeddings to be on device {device}, got {mask_embeddings.device}" - assert ( - mask_mask is None or mask_mask.device == device - ), f"Expected mask attn. mask to be on device {device}, got {mask_mask.device}" + assert box_embeddings is not None and box_embeddings.device == device, ( + f"Expected box embeddings to be on device {device}, got {box_embeddings.device}" + ) + assert box_mask is not None and box_mask.device == device, ( + f"Expected box mask to be on device {device}, got {box_mask.device}" + ) + assert box_labels is not None and box_labels.device == device, ( + f"Expected box labels to be on device {device}, got {box_labels.device}" + ) + assert point_embeddings is not None and point_embeddings.device == device, ( + f"Expected point embeddings to be on device {device}, got {point_embeddings.device}" + ) + assert point_mask is not None and point_mask.device == device, ( + f"Expected point mask to be on device {device}, got {point_mask.device}" + ) + assert point_labels is not None and point_labels.device == device, ( + f"Expected point labels to be on device {device}, got {point_labels.device}" + ) + assert mask_embeddings is None or mask_embeddings.device == device, ( + f"Expected mask embeddings to be on device {device}, got {mask_embeddings.device}" + ) + assert mask_mask is None or mask_mask.device == device, ( + f"Expected mask attn. mask to be on device {device}, got {mask_mask.device}" + ) self.box_embeddings = box_embeddings self.point_embeddings = point_embeddings @@ -263,30 +251,30 @@ def _init_seq_len_and_device( if point_embeddings is not None: point_seq_len = point_embeddings.shape[0] if bs is not None: - assert ( - bs == point_embeddings.shape[1] - ), f"Batch size mismatch between box and point embeddings. Got {bs} and {point_embeddings.shape[1]}." + assert bs == point_embeddings.shape[1], ( + f"Batch size mismatch between box and point embeddings. Got {bs} and {point_embeddings.shape[1]}." + ) else: bs = point_embeddings.shape[1] if device is not None: - assert ( - device == point_embeddings.device - ), "Device mismatch between box and point embeddings" + assert device == point_embeddings.device, ( + "Device mismatch between box and point embeddings" + ) else: device = point_embeddings.device if mask_embeddings is not None: mask_seq_len = mask_embeddings.shape[0] if bs is not None: - assert ( - bs == mask_embeddings.shape[1] - ), f"Batch size mismatch between box/point and mask embedding. Got {bs} and {mask_embeddings.shape[1]}" + assert bs == mask_embeddings.shape[1], ( + f"Batch size mismatch between box/point and mask embedding. Got {bs} and {mask_embeddings.shape[1]}" + ) else: bs = mask_embeddings.shape[1] if device is not None: - assert ( - device == mask_embeddings.device - ), "Device mismatch between box/point and mask embeddings." + assert device == mask_embeddings.device, ( + "Device mismatch between box/point and mask embeddings." + ) else: device = mask_embeddings.device @@ -538,9 +526,9 @@ def __init__( if add_cls: self.cls_embed = torch.nn.Embedding(1, self.d_model) - assert ( - points_direct_project or points_pos_enc or points_pool - ), "Error: need at least one way to encode points" + assert points_direct_project or points_pos_enc or points_pool, ( + "Error: need at least one way to encode points" + ) assert ( encode_boxes_as_points or boxes_direct_project @@ -582,16 +570,16 @@ def __init__( self.encode = None if num_layers > 0: - assert ( - add_cls - ), "It's currently highly recommended to add a CLS when using a transformer" + assert add_cls, ( + "It's currently highly recommended to add a CLS when using a transformer" + ) self.encode = get_clones(layer, num_layers) self.encode_norm = nn.LayerNorm(self.d_model) if mask_encoder is not None: - assert isinstance( - mask_encoder, MaskEncoder - ), f"Expected mask_encoder of type MaskEncoder. Got {type(mask_encoder)}." + assert isinstance(mask_encoder, MaskEncoder), ( + f"Expected mask_encoder of type MaskEncoder. Got {type(mask_encoder)}." + ) if add_mask_label: self.mask_label_embed = torch.nn.Embedding(2, self.d_model) self.add_mask_label = add_mask_label @@ -700,16 +688,15 @@ def _encode_masks( img_feats: torch.Tensor = None, ): n_masks, bs = masks.shape[:2] - assert ( - n_masks == 1 - ), "We assume one mask per prompt for now. Code should still be functional if this assertion is removed." - assert ( - list(attn_mask.shape) - == [ - bs, - n_masks, - ] - ), f"Expected attn_mask to be of shape {bs}x{n_masks}. Got {list(attn_mask.shape)}." + assert n_masks == 1, ( + "We assume one mask per prompt for now. Code should still be functional if this assertion is removed." + ) + assert list(attn_mask.shape) == [ + bs, + n_masks, + ], ( + f"Expected attn_mask to be of shape {bs}x{n_masks}. Got {list(attn_mask.shape)}." + ) masks, pos = self.mask_encoder( masks=masks.flatten(0, 1).float(), pix_feat=img_feats, diff --git a/sam3/model/maskformer_segmentation.py b/sam3/model/maskformer_segmentation.py index 4953b7c..2f03640 100644 --- a/sam3/model/maskformer_segmentation.py +++ b/sam3/model/maskformer_segmentation.py @@ -258,7 +258,9 @@ def __init__( self.d_model = hidden_dim if dot_product_scorer is not None: - assert presence_head, "Specifying a dot product scorer without a presence head is likely a mistake" + assert presence_head, ( + "Specifying a dot product scorer without a presence head is likely a mistake" + ) self.presence_head = None if presence_head: diff --git a/sam3/model/memory.py b/sam3/model/memory.py index 4901cbe..5540d85 100644 --- a/sam3/model/memory.py +++ b/sam3/model/memory.py @@ -68,9 +68,9 @@ def __init__( self.multiplex_count = multiplex_count self.interpol_size = interpol_size if self.interpol_size is not None: - assert isinstance( - self.interpol_size, (list, tuple) - ), f"Unsupported type {type(self.interpol_size)}. Should be a list or tuple." + assert isinstance(self.interpol_size, (list, tuple)), ( + f"Unsupported type {type(self.interpol_size)}. Should be a list or tuple." + ) self.interpol_size = list(interpol_size) assert len(self.interpol_size) == 2 diff --git a/sam3/model/model_misc.py b/sam3/model/model_misc.py index 6c01ab3..56d51c2 100644 --- a/sam3/model/model_misc.py +++ b/sam3/model/model_misc.py @@ -182,45 +182,45 @@ def multi_head_attention_forward( raise AssertionError( "only bool and floating types of key_padding_mask are supported" ) - assert ( - embed_dim == embed_dim_to_check - ), f"was expecting embedding dimension of {embed_dim_to_check}, but got {embed_dim}" + assert embed_dim == embed_dim_to_check, ( + f"was expecting embedding dimension of {embed_dim_to_check}, but got {embed_dim}" + ) if isinstance(embed_dim, torch.Tensor): head_dim = embed_dim.div(num_heads, rounding_mode="trunc") else: head_dim = embed_dim // num_heads - assert ( - head_dim * num_heads == embed_dim - ), f"embed_dim {embed_dim} not divisible by num_heads {num_heads}" + assert head_dim * num_heads == embed_dim, ( + f"embed_dim {embed_dim} not divisible by num_heads {num_heads}" + ) if use_separate_proj_weight: - assert ( - key.shape[:2] == value.shape[:2] - ), f"key's sequence and batch dims {key.shape[:2]} do not match value's {value.shape[:2]}" + assert key.shape[:2] == value.shape[:2], ( + f"key's sequence and batch dims {key.shape[:2]} do not match value's {value.shape[:2]}" + ) else: - assert ( - key.shape == value.shape - ), f"key shape {key.shape} does not match value shape {value.shape}" + assert key.shape == value.shape, ( + f"key shape {key.shape} does not match value shape {value.shape}" + ) # # compute in-projection # if not use_separate_proj_weight: - assert ( - in_proj_weight is not None - ), "use_separate_proj_weight is False but in_proj_weight is None" + assert in_proj_weight is not None, ( + "use_separate_proj_weight is False but in_proj_weight is None" + ) q, k, v = F._in_projection_packed( query, key, value, in_proj_weight, in_proj_bias ) else: - assert ( - q_proj_weight is not None - ), "use_separate_proj_weight is True but q_proj_weight is None" - assert ( - k_proj_weight is not None - ), "use_separate_proj_weight is True but k_proj_weight is None" - assert ( - v_proj_weight is not None - ), "use_separate_proj_weight is True but v_proj_weight is None" + assert q_proj_weight is not None, ( + "use_separate_proj_weight is True but q_proj_weight is None" + ) + assert k_proj_weight is not None, ( + "use_separate_proj_weight is True but k_proj_weight is None" + ) + assert v_proj_weight is not None, ( + "use_separate_proj_weight is True but v_proj_weight is None" + ) if in_proj_bias is None: b_q = b_k = b_v = None else: @@ -245,9 +245,9 @@ def multi_head_attention_forward( ) attn_mask = attn_mask.to(torch.bool) else: - assert ( - attn_mask.is_floating_point() or attn_mask.dtype == torch.bool - ), f"Only float, byte, and bool types are supported for attn_mask, not {attn_mask.dtype}" + assert attn_mask.is_floating_point() or attn_mask.dtype == torch.bool, ( + f"Only float, byte, and bool types are supported for attn_mask, not {attn_mask.dtype}" + ) # ensure attn_mask's dim is 3 if attn_mask.dim() == 2: correct_2d_size = (tgt_len, src_len) @@ -288,22 +288,22 @@ def multi_head_attention_forward( if static_k is None: k = k.contiguous().view(k.shape[0], bsz * num_heads, head_dim).transpose(0, 1) else: - assert ( - static_k.size(0) == bsz * num_heads - ), f"expecting static_k.size(0) of {bsz * num_heads}, but got {static_k.size(0)}" - assert ( - static_k.size(2) == head_dim - ), f"expecting static_k.size(2) of {head_dim}, but got {static_k.size(2)}" + assert static_k.size(0) == bsz * num_heads, ( + f"expecting static_k.size(0) of {bsz * num_heads}, but got {static_k.size(0)}" + ) + assert static_k.size(2) == head_dim, ( + f"expecting static_k.size(2) of {head_dim}, but got {static_k.size(2)}" + ) k = static_k if static_v is None: v = v.contiguous().view(v.shape[0], bsz * num_heads, head_dim).transpose(0, 1) else: - assert ( - static_v.size(0) == bsz * num_heads - ), f"expecting static_v.size(0) of {bsz * num_heads}, but got {static_v.size(0)}" - assert ( - static_v.size(2) == head_dim - ), f"expecting static_v.size(2) of {head_dim}, but got {static_v.size(2)}" + assert static_v.size(0) == bsz * num_heads, ( + f"expecting static_v.size(0) of {bsz * num_heads}, but got {static_v.size(0)}" + ) + assert static_v.size(2) == head_dim, ( + f"expecting static_v.size(2) of {head_dim}, but got {static_v.size(2)}" + ) v = static_v # add zero attention along batch dimension (now first) @@ -325,13 +325,12 @@ def multi_head_attention_forward( # merge key padding and attention masks if key_padding_mask is not None: - assert ( - key_padding_mask.shape - == ( - bsz, - src_len, - ) - ), f"expecting key_padding_mask shape of {(bsz, src_len)}, but got {key_padding_mask.shape}" + assert key_padding_mask.shape == ( + bsz, + src_len, + ), ( + f"expecting key_padding_mask shape of {(bsz, src_len)}, but got {key_padding_mask.shape}" + ) key_padding_mask = ( key_padding_mask.view(bsz, 1, 1, src_len) .expand(-1, num_heads, -1, -1) @@ -365,15 +364,14 @@ def multi_head_attention_forward( attn_mask = attn_mask.view(bsz, num_heads, -1, src_len) if attn_bias is not None: - assert ( - attn_bias.shape - == ( - bsz, - num_heads, - tgt_len, - src_len, - ) - ), f"expecting attn_bias shape of {(bsz, num_heads, tgt_len, src_len)}, but got {attn_bias.shape}" + assert attn_bias.shape == ( + bsz, + num_heads, + tgt_len, + src_len, + ), ( + f"expecting attn_bias shape of {(bsz, num_heads, tgt_len, src_len)}, but got {attn_bias.shape}" + ) if attn_mask is None: attn_mask = attn_bias else: @@ -487,13 +485,13 @@ def __init__( self.batch_first = batch_first self.head_dim = embed_dim // num_heads self.use_act_checkpoint = use_act_checkpoint - assert ( - self.head_dim * num_heads == self.embed_dim - ), "embed_dim must be divisible by num_heads" + assert self.head_dim * num_heads == self.embed_dim, ( + "embed_dim must be divisible by num_heads" + ) - assert ( - attn_type == AttentionType.Sparse or sparsity == 0.0 - ), "sparsity is only supported for sparse attention" + assert attn_type == AttentionType.Sparse or sparsity == 0.0, ( + "sparsity is only supported for sparse attention" + ) if not self._qkv_same_embed_dim: self.q_proj_weight = nn.Parameter( @@ -1007,9 +1005,9 @@ def __init__( self.output = output else: self.output = [] - assert isinstance( - iter_mode, SAM3Output.IterMode - ), f"iter_mode shoulf be of enum type 'SAM3Output.IterMode'. Got {type(iter_mode)}" + assert isinstance(iter_mode, SAM3Output.IterMode), ( + f"iter_mode shoulf be of enum type 'SAM3Output.IterMode'. Got {type(iter_mode)}" + ) self.iter_mode = iter_mode # We create a weak reference to self to be used in the lambda functions. @@ -1088,9 +1086,9 @@ def iteration_mode( return SAM3Output._IterationMode(model_output=model_output, iter_mode=iter_mode) def append(self, item: list): - assert isinstance( - item, list - ), f"Only list items are supported. Got {type(item)}" + assert isinstance(item, list), ( + f"Only list items are supported. Got {type(item)}" + ) self.output.append(item) def __repr__(self): diff --git a/sam3/model/multiplex_mask_decoder.py b/sam3/model/multiplex_mask_decoder.py index c584675..d1f4481 100644 --- a/sam3/model/multiplex_mask_decoder.py +++ b/sam3/model/multiplex_mask_decoder.py @@ -64,9 +64,9 @@ def __init__( ) if self.decode_mask_with_shared_tokens: - assert ( - multimask_outputs_only - ), "multimask_outputs_only must be True if decode_mask_with_shared_tokens" + assert multimask_outputs_only, ( + "multimask_outputs_only must be True if decode_mask_with_shared_tokens" + ) if self.multimask_outputs_only: self.num_mask_output_per_object = num_multimask_outputs @@ -169,14 +169,14 @@ def forward( """ if self.num_multimask_outputs <= 0: - assert ( - not multimask_output - ), f"multimask_output must be False with {self.num_multimask_outputs=}" + assert not multimask_output, ( + f"multimask_output must be False with {self.num_multimask_outputs=}" + ) if self.multimask_outputs_only: - assert ( - multimask_output - ), f"multimask_output must be True with {self.multimask_outputs_only=}" + assert multimask_output, ( + f"multimask_output must be True with {self.multimask_outputs_only=}" + ) out = self.predict_masks( image_embeddings=image_embeddings, @@ -223,19 +223,19 @@ def forward( out["sam_tokens_out"] = sam_tokens_out if multimask_output: - assert ( - masks.shape[2] == self.num_mask_output_per_object - ), f"{masks.shape=}, {self.num_mask_output_per_object=}" - assert ( - iou_pred.shape[2] == self.num_mask_output_per_object - ), f"{iou_pred.shape=}, {self.num_mask_output_per_object=}" + assert masks.shape[2] == self.num_mask_output_per_object, ( + f"{masks.shape=}, {self.num_mask_output_per_object=}" + ) + assert iou_pred.shape[2] == self.num_mask_output_per_object, ( + f"{iou_pred.shape=}, {self.num_mask_output_per_object=}" + ) if self.use_multimask_token_for_obj_ptr: if self.decode_mask_with_shared_tokens: assert sam_tokens_out.shape[2] == 1, f"{sam_tokens_out.shape=}" else: - assert ( - sam_tokens_out.shape[2] == self.num_mask_output_per_object - ), f"{sam_tokens_out.shape=}, {self.num_mask_output_per_object=}" + assert sam_tokens_out.shape[2] == self.num_mask_output_per_object, ( + f"{sam_tokens_out.shape=}, {self.num_mask_output_per_object=}" + ) else: assert masks.shape[2] == 1, f"{masks.shape=}" assert iou_pred.shape[2] == 1, f"{iou_pred.shape=}" @@ -278,9 +278,9 @@ def predict_masks( src = image_embeddings - assert ( - image_pe.size(0) == 1 - ), "image_pe should have size 1 in batch dim (from `get_dense_pe()`)" + assert image_pe.size(0) == 1, ( + "image_pe should have size 1 in batch dim (from `get_dense_pe()`)" + ) pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0) b, c, h, w = src.shape @@ -289,9 +289,9 @@ def predict_masks( # Parse transformer outputs based on token sharing configuration if self.decode_mask_attribute_with_shared_tokens: - assert ( - hs.shape[1] == self.num_mask_tokens - ), f"{hs.shape=}, {self.num_mask_tokens=}" + assert hs.shape[1] == self.num_mask_tokens, ( + f"{hs.shape=}, {self.num_mask_tokens=}" + ) iou_token_out = mask_tokens_out = hs[:, 0 : self.num_mask_tokens] if self.pred_obj_scores: obj_score_token_out = mask_tokens_out @@ -305,9 +305,9 @@ def predict_masks( iou_token_out = hs[:, s : s + self.multiplex_count, :] s += self.multiplex_count mask_tokens_out = hs[:, s : s + self.num_mask_tokens, :] - assert ( - hs.shape[1] == s + self.num_mask_tokens - ), f"{hs.shape=}, {s=}, {self.num_mask_tokens=}" + assert hs.shape[1] == s + self.num_mask_tokens, ( + f"{hs.shape=}, {s=}, {self.num_mask_tokens=}" + ) # Upscale mask embeddings and predict masks using the mask tokens src = src.transpose(1, 2).view(b, c, h, w) diff --git a/sam3/model/multiplex_utils.py b/sam3/model/multiplex_utils.py index 44f102a..73f5866 100644 --- a/sam3/model/multiplex_utils.py +++ b/sam3/model/multiplex_utils.py @@ -93,22 +93,22 @@ def _initialize_assignments( # check the validity of the object IDs self.object_ids = object_ids if self.object_ids is not None: - assert ( - len(self.object_ids) == self.total_valid_entries - ), "object_ids should map 1:1 to the valid entries" + assert len(self.object_ids) == self.total_valid_entries, ( + "object_ids should map 1:1 to the valid entries" + ) # check the validity of the assignments all_object_idxs = set() for bucket in self.assignments: valid_entries_in_bucket = sum(1 for x in bucket if x != _PADDING_NUM) - assert ( - valid_entries_in_bucket <= self.allowed_bucket_capacity - ), f"{valid_entries_in_bucket=} > {self.allowed_bucket_capacity=}" + assert valid_entries_in_bucket <= self.allowed_bucket_capacity, ( + f"{valid_entries_in_bucket=} > {self.allowed_bucket_capacity=}" + ) for obj_idx in bucket: if obj_idx >= 0: - assert ( - obj_idx < self.total_non_padding_entries - ), f"object ID {obj_idx} >= {self.total_non_padding_entries}" + assert obj_idx < self.total_non_padding_entries, ( + f"object ID {obj_idx} >= {self.total_non_padding_entries}" + ) assert obj_idx not in all_object_idxs, "object IDs must be unique" all_object_idxs.add(obj_idx) @@ -135,9 +135,9 @@ def find_next_batch_of_available_indices( # as the actual bucket allocation logic is in add_objects() assert num_objects > 0, f"{num_objects=} must be positive" if not allow_new_buckets: - assert ( - self.available_slots >= num_objects - ), f"not enough available slots {self.available_slots} < {num_objects}" + assert self.available_slots >= num_objects, ( + f"not enough available slots {self.available_slots} < {num_objects}" + ) return list( range( @@ -167,14 +167,14 @@ def add_objects( # we will modify this in-place object_indices = object_indices.copy() - assert (object_ids is None) == ( - self.object_ids is None - ), "object_ids must either be always given or always omitted" + assert (object_ids is None) == (self.object_ids is None), ( + "object_ids must either be always given or always omitted" + ) if object_ids is not None: - assert len(object_ids) == len( - object_indices - ), "object_ids must have the same length as object_indices" + assert len(object_ids) == len(object_indices), ( + "object_ids must have the same length as object_indices" + ) object_ids = object_ids.copy() num_new_objects = len(object_indices) @@ -225,9 +225,9 @@ def _pop_next(): # reinitialize all the settings original_num_entries = self.total_valid_entries self._initialize_assignments(self.assignments, object_ids=self.object_ids) - assert ( - self.total_valid_entries == original_num_entries + num_new_objects - ), f"{self.total_valid_entries=} != {original_num_entries=} + {num_new_objects=}" + assert self.total_valid_entries == original_num_entries + num_new_objects, ( + f"{self.total_valid_entries=} != {original_num_entries=} + {num_new_objects=}" + ) logger.info( f"Filled {slots_filled} slots and created {buckets_created} new buckets" @@ -258,9 +258,9 @@ def remove_objects(self, object_indices: list[int], strict: bool = True): object_indices.remove(obj_id) if strict: - assert ( - len(object_indices) == 0 - ), f"Failed to remove objects: {object_indices}" + assert len(object_indices) == 0, ( + f"Failed to remove objects: {object_indices}" + ) # Check which buckets should be completely removed (all objects removed/paddings) # and which buckets we will keep @@ -371,9 +371,9 @@ def mux(self, x: torch.Tensor) -> torch.Tensor: with padding entries filled with 0 """ num_valid_entries = x.shape[0] - assert ( - num_valid_entries == self.total_valid_entries - ), f"{num_valid_entries=} != {self.total_valid_entries=}" + assert num_valid_entries == self.total_valid_entries, ( + f"{num_valid_entries=} != {self.total_valid_entries=}" + ) output_shape = ( self.num_buckets, self.multiplex_count, @@ -396,9 +396,9 @@ def demux(self, x: torch.Tensor) -> torch.Tensor: """ num_buckets, multiplex_count = x.shape[:2] assert num_buckets == self.num_buckets, f"{num_buckets=} != {self.num_buckets=}" - assert ( - multiplex_count == self.multiplex_count - ), f"{multiplex_count=} != {self.multiplex_count=}" + assert multiplex_count == self.multiplex_count, ( + f"{multiplex_count=} != {self.multiplex_count=}" + ) output_shape = (self.total_valid_entries,) + x.shape[2:] x_flat = x.reshape(num_buckets * multiplex_count, -1) diff --git a/sam3/model/sam1_task_predictor.py b/sam3/model/sam1_task_predictor.py index edcbf87..5cf0fde 100644 --- a/sam3/model/sam1_task_predictor.py +++ b/sam3/model/sam1_task_predictor.py @@ -94,9 +94,9 @@ def set_image( input_image = self._transforms(image) input_image = input_image[None, ...].to(self.device) - assert ( - len(input_image.shape) == 4 and input_image.shape[1] == 3 - ), f"input_image must be of size 1x3xHxW, got {input_image.shape}" + assert len(input_image.shape) == 4 and input_image.shape[1] == 3, ( + f"input_image must be of size 1x3xHxW, got {input_image.shape}" + ) logging.info("Computing image embeddings for the provided image...") backbone_out = self.model.forward_image(input_image) ( @@ -133,17 +133,17 @@ def set_image_batch( assert isinstance(image_list, list) self._orig_hw = [] for image in image_list: - assert isinstance( - image, np.ndarray - ), "Images are expected to be an np.ndarray in RGB format, and of shape HWC" + assert isinstance(image, np.ndarray), ( + "Images are expected to be an np.ndarray in RGB format, and of shape HWC" + ) self._orig_hw.append(image.shape[:2]) # Transform the image to the form expected by the model img_batch = self._transforms.forward_batch(image_list) img_batch = img_batch.to(self.device) batch_size = img_batch.shape[0] - assert ( - len(img_batch.shape) == 4 and img_batch.shape[1] == 3 - ), f"img_batch must be of size Bx3xHxW, got {img_batch.shape}" + assert len(img_batch.shape) == 4 and img_batch.shape[1] == 3, ( + f"img_batch must be of size Bx3xHxW, got {img_batch.shape}" + ) logging.info("Computing image embeddings for the provided images...") backbone_out = self.model.forward_image(img_batch) ( @@ -299,9 +299,9 @@ def _prep_prompts( ): unnorm_coords, labels, unnorm_box, mask_input = None, None, None, None if point_coords is not None: - assert ( - point_labels is not None - ), "point_labels must be supplied if point_coords is supplied." + assert point_labels is not None, ( + "point_labels must be supplied if point_coords is supplied." + ) point_coords = torch.as_tensor( point_coords, dtype=torch.float, device=self.device ) @@ -438,9 +438,9 @@ def get_image_embedding(self) -> torch.Tensor: raise RuntimeError( "An image must be set with .set_image(...) to generate an embedding." ) - assert ( - self._features is not None - ), "Features must exist if an image has been set." + assert self._features is not None, ( + "Features must exist if an image has been set." + ) return self._features["image_embed"] @property diff --git a/sam3/model/sam3_image.py b/sam3/model/sam3_image.py index 9371432..ee13d2f 100644 --- a/sam3/model/sam3_image.py +++ b/sam3/model/sam3_image.py @@ -684,9 +684,9 @@ def predict_inst_batch( inference_state["original_heights"], inference_state["original_widths"], ) - assert ( - batch_size == len(orig_heights) == len(orig_widths) - ), f"Batch size mismatch in predict_inst_batch. Got {batch_size}, {len(orig_heights)}, {len(orig_widths)}" + assert batch_size == len(orig_heights) == len(orig_widths), ( + f"Batch size mismatch in predict_inst_batch. Got {batch_size}, {len(orig_heights)}, {len(orig_widths)}" + ) feats = [ feat.permute(1, 2, 0).view(batch_size, -1, *feat_size) for feat, feat_size in zip( diff --git a/sam3/model/sam3_image_processor.py b/sam3/model/sam3_image_processor.py index d3b6206..83bbb0d 100644 --- a/sam3/model/sam3_image_processor.py +++ b/sam3/model/sam3_image_processor.py @@ -81,9 +81,9 @@ def set_image_batch(self, images: List[np.ndarray], state=None): if not isinstance(images, list): raise ValueError("Images must be a list of PIL images or tensors") assert len(images) > 0, "Images list must not be empty" - assert isinstance( - images[0], PIL.Image.Image - ), "Images must be a list of PIL images" + assert isinstance(images[0], PIL.Image.Image), ( + "Images must be a list of PIL images" + ) state["original_heights"] = [image.height for image in images] state["original_widths"] = [image.width for image in images] diff --git a/sam3/model/sam3_multiplex_base.py b/sam3/model/sam3_multiplex_base.py index 3e3e639..e4df5d3 100644 --- a/sam3/model/sam3_multiplex_base.py +++ b/sam3/model/sam3_multiplex_base.py @@ -281,7 +281,9 @@ def __init__( assert ( self.is_multiplex == self.tracker.is_multiplex == self.detector.is_multiplex - ), f"is_multiplex must be the same for all models: {self.is_multiplex=}, {self.tracker.is_multiplex=}, {self.detector.is_multiplex=}" + ), ( + f"is_multiplex must be the same for all models: {self.is_multiplex=}, {self.tracker.is_multiplex=}, {self.detector.is_multiplex=}" + ) # hotstart parameters if hotstart_delay > 0: @@ -356,9 +358,9 @@ def __init__( ) if self.is_multiplex: - assert ( - not self.tracker.multiplex_controller.training - ), "This model class should only be used for eval." + assert not self.tracker.multiplex_controller.training, ( + "This model class should only be used for eval." + ) self.bucket_capacity: int = ( self.tracker.multiplex_controller.allowed_bucket_capacity ) @@ -1417,9 +1419,9 @@ def _suppress_overlapping_based_on_recent_occlusion( binary_tracker_low_res_masks_global = tracker_low_res_masks_global > 0 if batch_size > 0: - assert ( - len(obj_ids_global) == batch_size - ), f"Mismatch in number of objects: {len(obj_ids_global)} vs {batch_size}" + assert len(obj_ids_global) == batch_size, ( + f"Mismatch in number of objects: {len(obj_ids_global)} vs {batch_size}" + ) NEVER_OCCLUDED = -1 ALWAYS_OCCLUDED = 100000 # This value should be larger than any possible frame index, indicates that the object was removed by hotstart logic @@ -1595,9 +1597,9 @@ def _get_objects_to_suppress_based_on_most_recently_occluded( reverse: bool = False, ): # Suppress overlapping masks for objects that were most recently occluded - assert ( - binary_low_res_masks.dtype == torch.bool - ), f"Expected boolean tensor, got {binary_low_res_masks.dtype}" + assert binary_low_res_masks.dtype == torch.bool, ( + f"Expected boolean tensor, got {binary_low_res_masks.dtype}" + ) to_suppress = torch.zeros( binary_low_res_masks.size(0), device=binary_low_res_masks.device, @@ -1721,9 +1723,9 @@ def _propogate_tracker_one_frame_local_gpu( num_frames_propagated += 1 # only 1 frames should be propagated - assert ( - num_frames_propagated == 1 and out_frame_idx == frame_idx - ), f"num_frames_propagated: {num_frames_propagated}, out_frame_idx: {out_frame_idx}, frame_idx: {frame_idx}" + assert num_frames_propagated == 1 and out_frame_idx == frame_idx, ( + f"num_frames_propagated: {num_frames_propagated}, out_frame_idx: {out_frame_idx}, frame_idx: {frame_idx}" + ) assert isinstance(out_obj_ids, list) # Optionally filter to a subset of object ids (for partial propagation). # We also clamp indices to available rows to avoid CUDA index_select assertions. @@ -1867,9 +1869,9 @@ def _associate_det_trk( assert det_masks.is_floating_point(), "float tensor expected (do not binarize)" assert trk_masks.is_floating_point(), "float tensor expected (do not binarize)" - assert ( - trk_masks.size(0) == len(trk_obj_ids) - ), f"trk_masks and trk_obj_ids should have the same length, {trk_masks.size(0)} vs {len(trk_obj_ids)}" + assert trk_masks.size(0) == len(trk_obj_ids), ( + f"trk_masks and trk_obj_ids should have the same length, {trk_masks.size(0)} vs {len(trk_obj_ids)}" + ) if trk_masks.size(0) == 0: with torch.profiler.record_function("No tracklets"): num_trk = 0 @@ -2105,9 +2107,9 @@ def _process_hotstart_gpu( # New objects are added via extend_gpu_metadata_for_new_objects AFTER compaction, # so prev_N_obj should already include objects detected on previous frame. # N_obj should equal prev_N_obj (no new objects mid-planning-phase). - assert ( - N_obj == prev_N_obj - ), f"N_obj ({N_obj}) should equal prev_N_obj ({prev_N_obj}); new objects handled after compaction" + assert N_obj == prev_N_obj, ( + f"N_obj ({N_obj}) should equal prev_N_obj ({prev_N_obj}); new objects handled after compaction" + ) # Carry forward existing metadata (or initialize if first frame) NEVER_OCCLUDED = -1 diff --git a/sam3/model/sam3_multiplex_detector.py b/sam3/model/sam3_multiplex_detector.py index be06d20..435d6d6 100644 --- a/sam3/model/sam3_multiplex_detector.py +++ b/sam3/model/sam3_multiplex_detector.py @@ -37,9 +37,9 @@ def forward( input: BatchedDatapoint, is_inference=False, # (a dummy parameter not used anymore) ): - assert ( - not self.training - ), "Sam3MultiplexImageBase should only be used in eval mode." + assert not self.training, ( + "Sam3MultiplexImageBase should only be used in eval mode." + ) device = self.device backbone_out = {"img_batch_all_stages": input.img_batch} diff --git a/sam3/model/sam3_multiplex_tracking.py b/sam3/model/sam3_multiplex_tracking.py index d722e1a..35183a5 100644 --- a/sam3/model/sam3_multiplex_tracking.py +++ b/sam3/model/sam3_multiplex_tracking.py @@ -1250,9 +1250,9 @@ def _build_sam2_output( # Update with refined masks if provided if refined_obj_id_to_mask is not None: for obj_id, refined_mask in refined_obj_id_to_mask.items(): - assert ( - refined_mask is not None - ), f"Refined mask data must be provided for obj_id {obj_id}" + assert refined_mask is not None, ( + f"Refined mask data must be provided for obj_id {obj_id}" + ) obj_id_to_mask[obj_id] = refined_mask return obj_id_to_mask @@ -1678,18 +1678,18 @@ def add_prompt( device = inference_state["device"] num_frames = inference_state["num_frames"] - assert ( - text_str is not None or points is not None or boxes_xywh is not None - ), "at least one type of prompt (text, points, boxes) must be provided" - assert ( - 0 <= frame_idx < num_frames - ), f"{frame_idx=} is out of range for a total of {num_frames} frames" + assert text_str is not None or points is not None or boxes_xywh is not None, ( + "at least one type of prompt (text, points, boxes) must be provided" + ) + assert 0 <= frame_idx < num_frames, ( + f"{frame_idx=} is out of range for a total of {num_frames} frames" + ) assert clear_old_boxes, "clear old boxes must be True" - assert ( - points is None and clear_old_points is True and point_labels is None - ), "Point prompts not accepted" + assert points is None and clear_old_points is True and point_labels is None, ( + "Point prompts not accepted" + ) # since it's a semantic prompt, we start over self.reset_state(inference_state) @@ -2484,9 +2484,9 @@ def add_action_history( "propagation_fetch", "propagation_cancel", ] - assert ( - action_type in instance_actions + propagation_actions - ), f"Invalid action type: {action_type}, must be one of {instance_actions + propagation_actions}" + assert action_type in instance_actions + propagation_actions, ( + f"Invalid action type: {action_type}, must be one of {instance_actions + propagation_actions}" + ) action = { "type": action_type, "frame_idx": frame_idx, @@ -2518,7 +2518,7 @@ def parse_action_history_for_propagation(self, inference_state): ): # last action is cancel, we go back to the action before cancel action_before_cancelation = inference_state["action_history"][-2] - # the action before cancelation can be a propagation_fetch from running both forward + # the action before cancellation can be a propagation_fetch from running both forward # and backward propagation as in webdemo interface, in that case we go back one more step if action_before_cancelation["type"] == "propagation_fetch": action_before_cancelation = inference_state["action_history"][-3] @@ -2700,12 +2700,12 @@ def add_prompt( ): if points is not None: # SAM2 instance prompts - assert ( - text_str is None and boxes_xywh is None - ), "When points are provided, text_str and boxes_xywh must be None." - assert ( - obj_id is not None - ), "When points are provided, obj_id must be provided." + assert text_str is None and boxes_xywh is None, ( + "When points are provided, text_str and boxes_xywh must be None." + ) + assert obj_id is not None, ( + "When points are provided, obj_id must be provided." + ) return self.add_sam2_new_points( inference_state, frame_idx, @@ -2846,9 +2846,9 @@ def add_sam2_new_points( tracker_states = self._get_sam2_inference_states_by_obj_ids( inference_state, [obj_id] ) - assert ( - len(tracker_states) == 1 - ), f"[rank={self.rank}] Multiple SAM2 inference states found for the same object id." + assert len(tracker_states) == 1, ( + f"[rank={self.rank}] Multiple SAM2 inference states found for the same object id." + ) sam2_state = tracker_states[0] # log diff --git a/sam3/model/sam3_video_base.py b/sam3/model/sam3_video_base.py index a77bdd3..e3b40ca 100644 --- a/sam3/model/sam3_video_base.py +++ b/sam3/model/sam3_video_base.py @@ -177,7 +177,9 @@ def _associate_det_trk_compilable( else: intersection_metric = mask_iou(det_masks_binary, trk_masks_binary) # (N, M) - assert not o2o_matching_masklets_enable, "Temporarily disabled support for o2o_matching_masklets_enable, due to optimizations." + assert not o2o_matching_masklets_enable, ( + "Temporarily disabled support for o2o_matching_masklets_enable, due to optimizations." + ) if o2o_matching_masklets_enable: intersection_metric_np = intersection_metric.cpu().numpy() @@ -830,9 +832,9 @@ def run_tracker_update_planning_phase( num_obj_dropped_due_to_limit, trk_id_to_max_iou_high_conf_det, ] - assert ( - len(update_plan) == NUM_BROADCAST_ITEMS - ), f"Manually update NUM_BROADCAST_ITEMS to be: {len(update_plan)}" + assert len(update_plan) == NUM_BROADCAST_ITEMS, ( + f"Manually update NUM_BROADCAST_ITEMS to be: {len(update_plan)}" + ) self.broadcast_python_obj_cpu(update_plan, src=0) elif self.rank > 0 and self.world_size > 1: update_plan = [ @@ -1052,9 +1054,9 @@ def _suppress_overlapping_based_on_recent_occlusion( binary_tracker_low_res_masks_global = tracker_low_res_masks_global > 0 batch_size = tracker_low_res_masks_global.size(0) if batch_size > 0: - assert ( - len(obj_ids_global) == batch_size - ), f"Mismatch in number of objects: {len(obj_ids_global)} vs {batch_size}" + assert len(obj_ids_global) == batch_size, ( + f"Mismatch in number of objects: {len(obj_ids_global)} vs {batch_size}" + ) NEVER_OCCLUDED = -1 ALWAYS_OCCLUDED = 100000 # This value should be larger than any possible frame index, indicates that the object was removed by hotstart logic last_occluded_prev = torch.cat( @@ -1257,9 +1259,9 @@ def _get_objects_to_suppress_based_on_most_recently_occluded( reverse: bool = False, ): # Suppress overlapping masks for objects that were most recently occluded - assert ( - binary_low_res_masks.dtype == torch.bool - ), f"Expected boolean tensor, got {binary_low_res_masks.dtype}" + assert binary_low_res_masks.dtype == torch.bool, ( + f"Expected boolean tensor, got {binary_low_res_masks.dtype}" + ) to_suppress = torch.zeros( binary_low_res_masks.size(0), device=binary_low_res_masks.device, @@ -1364,9 +1366,9 @@ def _propogate_tracker_one_frame_local_gpu( num_frames_propagated += 1 # only 1 frames should be propagated - assert ( - num_frames_propagated == 1 and out_frame_idx == frame_idx - ), f"num_frames_propagated: {num_frames_propagated}, out_frame_idx: {out_frame_idx}, frame_idx: {frame_idx}" + assert num_frames_propagated == 1 and out_frame_idx == frame_idx, ( + f"num_frames_propagated: {num_frames_propagated}, out_frame_idx: {out_frame_idx}, frame_idx: {frame_idx}" + ) assert isinstance(out_obj_ids, list) obj_ids_local.extend(out_obj_ids) low_res_masks_list.append(out_low_res_masks.squeeze(1)) @@ -1424,9 +1426,9 @@ def _associate_det_trk( assert det_masks.is_floating_point(), "float tensor expected (do not binarize)" assert trk_masks.is_floating_point(), "float tensor expected (do not binarize)" - assert ( - trk_masks.size(0) == len(trk_obj_ids) - ), f"trk_masks and trk_obj_ids should have the same length, {trk_masks.size(0)} vs {len(trk_obj_ids)}" + assert trk_masks.size(0) == len(trk_obj_ids), ( + f"trk_masks and trk_obj_ids should have the same length, {trk_masks.size(0)} vs {len(trk_obj_ids)}" + ) if trk_masks.size(0) == 0: # all detections are new new_det_fa_inds = np.arange(det_masks.size(0)) @@ -1883,9 +1885,9 @@ def update_masklet_confirmation_status( # a) first, expand "confirmation_data" to include new masklets added in this frame status_prev = confirmation_data["status"] consecutive_det_num_prev = confirmation_data["consecutive_det_num"] - assert ( - status_prev.shape == obj_ids_all_gpu_prev.shape - ), f"Got {status_prev.shape} vs {obj_ids_all_gpu_prev.shape}" + assert status_prev.shape == obj_ids_all_gpu_prev.shape, ( + f"Got {status_prev.shape} vs {obj_ids_all_gpu_prev.shape}" + ) obj_id_to_updated_idx = { obj_id: idx for idx, obj_id in enumerate(obj_ids_all_gpu_updated) diff --git a/sam3/model/sam3_video_inference.py b/sam3/model/sam3_video_inference.py index f707505..18cd387 100644 --- a/sam3/model/sam3_video_inference.py +++ b/sam3/model/sam3_video_inference.py @@ -554,7 +554,9 @@ def _build_tracker_output( assert ( "cached_frame_outputs" in inference_state and frame_idx in inference_state["cached_frame_outputs"] - ), "No cached outputs found. Ensure normal propagation has run first to populate the cache." + ), ( + "No cached outputs found. Ensure normal propagation has run first to populate the cache." + ) cached_outputs = inference_state["cached_frame_outputs"][frame_idx] obj_id_to_mask = cached_outputs.copy() @@ -562,9 +564,9 @@ def _build_tracker_output( # Update with refined masks if provided if refined_obj_id_to_mask is not None: for obj_id, refined_mask in refined_obj_id_to_mask.items(): - assert ( - refined_mask is not None - ), f"Refined mask data must be provided for obj_id {obj_id}" + assert refined_mask is not None, ( + f"Refined mask data must be provided for obj_id {obj_id}" + ) obj_id_to_mask[obj_id] = refined_mask return obj_id_to_mask @@ -853,12 +855,12 @@ def add_prompt( logger.debug("Running add_prompt on frame %d", frame_idx) num_frames = inference_state["num_frames"] - assert ( - text_str is not None or boxes_xywh is not None - ), "at least one type of prompt (text, boxes) must be provided" - assert ( - 0 <= frame_idx < num_frames - ), f"{frame_idx=} is out of range for a total of {num_frames} frames" + assert text_str is not None or boxes_xywh is not None, ( + "at least one type of prompt (text, boxes) must be provided" + ) + assert 0 <= frame_idx < num_frames, ( + f"{frame_idx=} is out of range for a total of {num_frames} frames" + ) # since it's a semantic prompt, we start over self.reset_state(inference_state) @@ -1199,9 +1201,9 @@ def add_action_history( "propagation_partial", "propagation_fetch", ] - assert ( - action_type in instance_actions + propagation_actions - ), f"Invalid action type: {action_type}, must be one of {instance_actions + propagation_actions}" + assert action_type in instance_actions + propagation_actions, ( + f"Invalid action type: {action_type}, must be one of {instance_actions + propagation_actions}" + ) action = { "type": action_type, "frame_idx": frame_idx, @@ -1369,12 +1371,12 @@ def add_prompt( ): if points is not None: # Tracker instance prompts - assert ( - text_str is None and boxes_xywh is None - ), "When points are provided, text_str and boxes_xywh must be None." - assert ( - obj_id is not None - ), "When points are provided, obj_id must be provided." + assert text_str is None and boxes_xywh is None, ( + "When points are provided, text_str and boxes_xywh must be None." + ) + assert obj_id is not None, ( + "When points are provided, obj_id must be provided." + ) return self.add_tracker_new_points( inference_state, frame_idx, @@ -1490,9 +1492,9 @@ def add_tracker_new_points( tracker_states = self._get_tracker_inference_states_by_obj_ids( inference_state, [obj_id] ) - assert ( - len(tracker_states) == 1 - ), f"[rank={self.rank}] Multiple Tracker inference states found for the same object id." + assert len(tracker_states) == 1, ( + f"[rank={self.rank}] Multiple Tracker inference states found for the same object id." + ) tracker_state = tracker_states[0] # log diff --git a/sam3/model/text_encoder_ve.py b/sam3/model/text_encoder_ve.py index 53ddd5d..5788358 100644 --- a/sam3/model/text_encoder_ve.py +++ b/sam3/model/text_encoder_ve.py @@ -318,9 +318,9 @@ def forward( # The text is already encoded, use as is. text_attention_mask, text_memory_resized, tokenized = text inputs_embeds = tokenized["inputs_embeds"] - assert ( - input_boxes is None or len(input_boxes) == 0 - ), "Can't replace boxes in text if it's already encoded" + assert input_boxes is None or len(input_boxes) == 0, ( + "Can't replace boxes in text if it's already encoded" + ) # Note that the input_embeds are returned in pytorch's convention (sequence first) return ( diff --git a/sam3/model/video_tracking_multiplex.py b/sam3/model/video_tracking_multiplex.py index 0a4b0eb..8706d79 100644 --- a/sam3/model/video_tracking_multiplex.py +++ b/sam3/model/video_tracking_multiplex.py @@ -318,9 +318,9 @@ def __init__( ): # if there is compression of memories along channel dim mem_dim = self.maskmem_backbone.out_proj.weight.shape[0] - assert ( - mem_dim == self.hidden_dim - ), "there should be no compression of memory embeddings" + assert mem_dim == self.hidden_dim, ( + "there should be no compression of memory embeddings" + ) self.num_maskmem = num_maskmem # Number of memories accessible # Temporal encoding of the memories self.sincos_tpos_enc = sincos_tpos_enc @@ -396,9 +396,9 @@ def __init__( self.forward_backbone_per_frame_for_eval = forward_backbone_per_frame_for_eval self.offload_output_to_cpu_for_eval = offload_output_to_cpu_for_eval if trim_past_non_cond_mem_for_eval: - assert ( - num_frames_to_correct_for_eval <= 1 - ), "trim_past_non_cond_mem_for_eval=True requires that only the first frame receives prompts" + assert num_frames_to_correct_for_eval <= 1, ( + "trim_past_non_cond_mem_for_eval=True requires that only the first frame receives prompts" + ) self.trim_past_non_cond_mem_for_eval = trim_past_non_cond_mem_for_eval self.sam_mask_decoder_extra_args = sam_mask_decoder_extra_args self.interactive_sam_mask_decoder_extra_args = ( @@ -940,9 +940,9 @@ def _use_mask_as_output( # Use -10/+10 as logits for neg/pos pixels (very close to 0/1 in prob after sigmoid). out_scale, out_bias = 20.0, -10.0 # sigmoid(-10.0)=4.5398e-05 mask_inputs_float = mask_inputs.to(backbone_features.dtype) - assert mask_inputs.shape[0] == len( - objects_in_mask - ), f"{mask_inputs.shape[0]} != {len(objects_in_mask)}" + assert mask_inputs.shape[0] == len(objects_in_mask), ( + f"{mask_inputs.shape[0]} != {len(objects_in_mask)}" + ) high_res_masks = mask_inputs_float * out_scale + out_bias low_res_masks = F.interpolate( high_res_masks, @@ -1541,9 +1541,9 @@ def _prepare_memory_conditioned_features( # expand to batch size obj_pos = obj_pos.unsqueeze(1).expand(-1, B, -1) - assert ( - self.mem_dim == C - ), f"obj_ptrs.shape = {obj_ptrs.shape}, C = {C}" + assert self.mem_dim == C, ( + f"obj_ptrs.shape = {obj_ptrs.shape}, C = {C}" + ) # each frame has [bucket_size] pointers, except the first frame obj_pos = obj_pos.repeat_interleave( @@ -1640,9 +1640,9 @@ def _encode_new_memory( ) if self.apply_sigmoid_to_mask_logits_for_mem_enc: # scale the raw mask logits with a temperature before applying sigmoid - assert ( - not self.binarize_mask_from_pts_for_mem_enc - ), "haven't been trained this way; beware of hardcoded config override" + assert not self.binarize_mask_from_pts_for_mem_enc, ( + "haven't been trained this way; beware of hardcoded config override" + ) binarize = self.binarize_mask_from_pts_for_mem_enc and is_mask_from_pts if binarize and not self.training: mask_for_mem = (pred_masks_high_res > 0).float() @@ -1962,9 +1962,9 @@ def _track_step_aux( elif point_inputs is not None: # Case 3a: Refining existing predictions if prev_sam_mask_logits is not None: - assert ( - objects_to_interact is not None - ), "objects_to_interact must be specified when refining with prev_sam_mask_logits" + assert objects_to_interact is not None, ( + "objects_to_interact must be specified when refining with prev_sam_mask_logits" + ) mode = "interaction_only" # Case 3b: Initial conditioning frame elif is_init_cond_frame: @@ -2271,9 +2271,9 @@ def _track_step_aux( method="uniform" if self.training else self.pt_sampling_for_eval, ) point_inputs = concat_points(point_inputs, new_points, new_labels) - assert low_res_masks.shape[0] > max( - objects_to_interact - ), f"interacting {objects_to_interact} in {low_res_masks.shape}?" + assert low_res_masks.shape[0] > max(objects_to_interact), ( + f"interacting {objects_to_interact} in {low_res_masks.shape}?" + ) if self.iter_use_prev_mask_pred: # Feed the mask logits of the previous SAM outputs in the next SAM decoder step. # For tracking, this means that when the user adds a correction click, we also feed @@ -2858,9 +2858,9 @@ def prepare_prompt_inputs(self, backbone_out, input, start_frame_idx=0): else: # During evaluation, this should only happen for YouTubeVOS. # We will skip the frames before the first conditional frame. - assert ( - self.is_dynamic_vos_evaluation - ), f"{visible_objects_per_frame=} invalid" + assert self.is_dynamic_vos_evaluation, ( + f"{visible_objects_per_frame=} invalid" + ) assert len(init_cond_frames) == 1 for stage_id in range(start_frame_idx, num_frames): if len(visible_objects_per_frame[stage_id]) > 0: @@ -3525,13 +3525,14 @@ def forward_tracking( new_object_idxs = new_idx_per_transition[stage_id] # Get the new object masks, ensure correct ordering assert sorted(new_object_idxs) == new_object_idxs - assert ( - new_object_idxs[0] - == len(valid_objects_prior_to_each_transition[stage_id]) - ), f"{new_object_idxs=}; {gt_masks.shape=}; {valid_objects_prior_to_each_transition[stage_id]=}" - assert new_object_idxs[-1] == ( - len(gt_masks) - 1 - ), f"{new_object_idxs=}; {gt_masks.shape=}" + assert new_object_idxs[0] == len( + valid_objects_prior_to_each_transition[stage_id] + ), ( + f"{new_object_idxs=}; {gt_masks.shape=}; {valid_objects_prior_to_each_transition[stage_id]=}" + ) + assert new_object_idxs[-1] == (len(gt_masks) - 1), ( + f"{new_object_idxs=}; {gt_masks.shape=}" + ) new_object_masks = gt_masks[new_object_idxs] # Remove the new objects from the gt masks diff --git a/sam3/model/vitdet.py b/sam3/model/vitdet.py index 90d42d7..f6771a1 100644 --- a/sam3/model/vitdet.py +++ b/sam3/model/vitdet.py @@ -838,9 +838,9 @@ def __init__( self.retain_cls_token = retain_cls_token if self.retain_cls_token: assert pretrain_use_cls_token - assert ( - len(window_block_indexes) == 0 - ), "windowing not supported with cls token" + assert len(window_block_indexes) == 0, ( + "windowing not supported with cls token" + ) assert sum(self.rel_pos_blocks) == 0, "rel pos not supported with cls token" diff --git a/sam3/model/vl_combiner.py b/sam3/model/vl_combiner.py index aed1f2c..b2cf102 100644 --- a/sam3/model/vl_combiner.py +++ b/sam3/model/vl_combiner.py @@ -185,9 +185,9 @@ def __init__(self, visual, text, compile_visual=False, scalp=0): super().__init__( visual=visual, text=text, compile_visual=compile_visual, scalp=scalp ) - assert isinstance( - self.vision_backbone, Sam3TriViTDetNeck - ), f"Expected vision backbone to be of type Sam3TriViTDetNeck, got {type(self.vision_backbone)}" + assert isinstance(self.vision_backbone, Sam3TriViTDetNeck), ( + f"Expected vision backbone to be of type Sam3TriViTDetNeck, got {type(self.vision_backbone)}" + ) def forward_image( self, @@ -356,9 +356,9 @@ def __init__( compile_mode=compile_mode, compile_extra_args=compile_extra_args, ) - assert isinstance( - self.vision_backbone, Sam3TriViTDetNeck - ), f"Expected vision backbone to be of type Sam3TriViTDetNeck, got {type(self.vision_backbone)}" + assert isinstance(self.vision_backbone, Sam3TriViTDetNeck), ( + f"Expected vision backbone to be of type Sam3TriViTDetNeck, got {type(self.vision_backbone)}" + ) def forward_image( self, diff --git a/sam3/perflib/connected_components.py b/sam3/perflib/connected_components.py index 0be67e5..fa0506d 100644 --- a/sam3/perflib/connected_components.py +++ b/sam3/perflib/connected_components.py @@ -36,9 +36,9 @@ def connected_components_cpu(input_tensor: torch.Tensor): if input_tensor.dim() == 4 and input_tensor.shape[1] == 1: input_tensor = input_tensor.squeeze(1) else: - assert ( - input_tensor.dim() == 3 - ), "Input tensor must be (B, H, W) or (B, 1, H, W)." + assert input_tensor.dim() == 3, ( + "Input tensor must be (B, H, W) or (B, 1, H, W)." + ) batch_size = input_tensor.shape[0] labels_list = [] @@ -67,9 +67,9 @@ def connected_components(input_tensor: torch.Tensor): if input_tensor.dim() == 3: input_tensor = input_tensor.unsqueeze(1) - assert ( - input_tensor.dim() == 4 and input_tensor.shape[1] == 1 - ), "Input tensor must be (B, H, W) or (B, 1, H, W)." + assert input_tensor.dim() == 4 and input_tensor.shape[1] == 1, ( + "Input tensor must be (B, H, W) or (B, 1, H, W)." + ) if input_tensor.is_cuda: if HAS_CC_TORCH: diff --git a/sam3/perflib/triton/connected_components.py b/sam3/perflib/triton/connected_components.py index cdb7d44..1d4376e 100644 --- a/sam3/perflib/triton/connected_components.py +++ b/sam3/perflib/triton/connected_components.py @@ -407,16 +407,16 @@ def connected_components_triton(input_tensor: torch.Tensor): - A BxHxW output tensor with dense labels. Background is 0. - A BxHxW tensor with the size of the connected component for each pixel. """ - assert ( - input_tensor.is_cuda and input_tensor.is_contiguous() - ), "Input tensor must be a contiguous CUDA tensor." + assert input_tensor.is_cuda and input_tensor.is_contiguous(), ( + "Input tensor must be a contiguous CUDA tensor." + ) out_shape = input_tensor.shape if input_tensor.dim() == 4 and input_tensor.shape[1] == 1: input_tensor = input_tensor.squeeze(1) else: - assert ( - input_tensor.dim() == 3 - ), "Input tensor must be (B, H, W) or (B, 1, H, W)." + assert input_tensor.dim() == 3, ( + "Input tensor must be (B, H, W) or (B, 1, H, W)." + ) B, H, W = input_tensor.shape numel = B * H * W diff --git a/sam3/sam/mask_decoder.py b/sam3/sam/mask_decoder.py index 3e1bbd2..944e57a 100644 --- a/sam3/sam/mask_decoder.py +++ b/sam3/sam/mask_decoder.py @@ -202,9 +202,9 @@ def predict_masks( assert image_embeddings.shape[0] == tokens.shape[0] src = image_embeddings src = src + dense_prompt_embeddings - assert ( - image_pe.size(0) == 1 - ), "image_pe should have size 1 in batch dim (from `get_dense_pe()`)" + assert image_pe.size(0) == 1, ( + "image_pe should have size 1 in batch dim (from `get_dense_pe()`)" + ) pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0) b, c, h, w = src.shape diff --git a/sam3/sam/transformer.py b/sam3/sam/transformer.py index 6716c54..1ff2380 100644 --- a/sam3/sam/transformer.py +++ b/sam3/sam/transformer.py @@ -204,9 +204,9 @@ def __init__( self.internal_dim = embedding_dim // downsample_rate self.num_heads = num_heads self.use_fa3 = use_fa3 - assert ( - self.internal_dim % num_heads == 0 - ), "num_heads must divide embedding_dim." + assert self.internal_dim % num_heads == 0, ( + "num_heads must divide embedding_dim." + ) self.q_proj = nn.Linear(embedding_dim, self.internal_dim) self.k_proj = nn.Linear(self.kv_in_dim, self.internal_dim) diff --git a/sam3/train/data/collator.py b/sam3/train/data/collator.py index 8ce4e28..38d031d 100644 --- a/sam3/train/data/collator.py +++ b/sam3/train/data/collator.py @@ -216,9 +216,9 @@ def collate_fn_api( text_batch.append(q.query_text) stages[stage_id].text_ids.append(text_batch.index(q.query_text)) - assert ( - q.inference_metadata is not None - ), "inference_metadata must be provided when FindQueryLoaded is created." + assert q.inference_metadata is not None, ( + "inference_metadata must be provided when FindQueryLoaded is created." + ) for f in fields(q.inference_metadata): getattr(find_metadatas[stage_id], f.name).append( getattr(q.inference_metadata, f.name) diff --git a/sam3/train/data/sam3_image_dataset.py b/sam3/train/data/sam3_image_dataset.py index dca5b97..28e7425 100644 --- a/sam3/train/data/sam3_image_dataset.py +++ b/sam3/train/data/sam3_image_dataset.py @@ -233,9 +233,9 @@ def set_sharded_annotation_file(self, data_epoch: int): if self.coco is not None: return - assert g_pathmgr.isfile( - self.annFile - ), f"please provide valid annotation file. Missing: {self.annFile}" + assert g_pathmgr.isfile(self.annFile), ( + f"please provide valid annotation file. Missing: {self.annFile}" + ) annFile = g_pathmgr.get_local_path(self.annFile) if self.coco is not None: @@ -325,9 +325,9 @@ def load_queries(self, pil_images, annotations, queries, img_metadata): else: num_queries_per_stage = stage2num_queries.most_common(1)[0][1] for stage, num_queries in stage2num_queries.items(): - assert ( - num_queries == num_queries_per_stage - ), f"Number of queries in stage {stage} is {num_queries}, expected {num_queries_per_stage}" + assert num_queries == num_queries_per_stage, ( + f"Number of queries in stage {stage} is {num_queries}, expected {num_queries_per_stage}" + ) for query in queries: h, w = id2imsize[query["image_id"]] diff --git a/sam3/train/data/sam3_video_dataset.py b/sam3/train/data/sam3_video_dataset.py index 3be37a4..75eee62 100644 --- a/sam3/train/data/sam3_video_dataset.py +++ b/sam3/train/data/sam3_video_dataset.py @@ -15,7 +15,6 @@ import torch import torchvision - # from decord import cpu, VideoReader from iopath.common.file_io import PathManager @@ -219,9 +218,9 @@ def _filter_query_and_anns( for query in filtered_queries: ptr_x_is_empty = query["ptr_x_query_id"] in [None, -1] ptr_y_is_empty = query["ptr_y_query_id"] in [None, -1] - assert ( - ptr_x_is_empty and ptr_y_is_empty - ), "Remapping stage ids is not supported for queries with non-empty ptr_x or ptr_y pointers" + assert ptr_x_is_empty and ptr_y_is_empty, ( + "Remapping stage ids is not supported for queries with non-empty ptr_x or ptr_y pointers" + ) query["query_processing_order"] = stage_id_old2new[ query["query_processing_order"] ] diff --git a/sam3/train/loss/loss_fns.py b/sam3/train/loss/loss_fns.py index ca8e348..8fa1774 100644 --- a/sam3/train/loss/loss_fns.py +++ b/sam3/train/loss/loss_fns.py @@ -324,7 +324,9 @@ def __init__( if num_det_queries is not None: logging.warning("note: it's not needed to set num_det_queries anymore") if self.use_separate_loss_for_det_and_trk: - assert not self.weak_loss, "Do not use weak_loss in this case -- set separate loss for detection and tracking queries instead" + assert not self.weak_loss, ( + "Do not use weak_loss in this case -- set separate loss for detection and tracking queries instead" + ) self.det_exhaustive_loss_scale_pos = det_exhaustive_loss_scale_pos self.det_exhaustive_loss_scale_neg = det_exhaustive_loss_scale_neg self.det_non_exhaustive_loss_scale_pos = det_non_exhaustive_loss_scale_pos @@ -339,7 +341,9 @@ def __init__( and det_non_exhaustive_loss_scale_neg == 1.0 and trk_loss_scale_pos == 1.0 and trk_loss_scale_neg == 1.0 - ), "If not using separate loss for detection and tracking queries, separate detection and tracking loss scales should all be 1.0" + ), ( + "If not using separate loss for detection and tracking queries, separate detection and tracking loss scales should all be 1.0" + ) def get_loss(self, outputs, targets, indices, num_boxes): assert len(outputs["pred_logits"].shape) > 2, "Incorrect predicted logits shape" @@ -440,7 +444,9 @@ def get_loss(self, outputs, targets, indices, num_boxes): pass if self.weak_loss: - assert not self.use_separate_loss_for_det_and_trk, "Do not use weak_loss in this case -- set separate loss for detection and tracking queries instead" + assert not self.use_separate_loss_for_det_and_trk, ( + "Do not use weak_loss in this case -- set separate loss for detection and tracking queries instead" + ) # nullify the negative loss for the non-exhaustive classes assert loss_bce.shape[0] == targets["is_exhaustive"].shape[0] @@ -494,9 +500,9 @@ def get_loss(self, outputs, targets, indices, num_boxes): loss_bce = loss_bce.mean() else: assert isinstance(self.pad_n_queries, int) - assert ( - loss_bce.size(1) < self.pad_n_queries - ), f"The number of predictions is more than the expected total after padding. Got {loss_bce.size(1)} predictions." + assert loss_bce.size(1) < self.pad_n_queries, ( + f"The number of predictions is more than the expected total after padding. Got {loss_bce.size(1)} predictions." + ) loss_bce = loss_bce.sum() / (self.pad_n_queries * loss_bce.size(0)) bce_f1 = torchmetrics.functional.f1_score( diff --git a/sam3/train/masks_ops.py b/sam3/train/masks_ops.py index 4e966bd..eefc17b 100644 --- a/sam3/train/masks_ops.py +++ b/sam3/train/masks_ops.py @@ -119,9 +119,9 @@ def dilation(mask, kernel_size): assert mask.ndim == 3 kernel_size = int(kernel_size) - assert ( - kernel_size % 2 == 1 - ), f"Dilation expects a odd kernel size, got {kernel_size}" + assert kernel_size % 2 == 1, ( + f"Dilation expects a odd kernel size, got {kernel_size}" + ) if mask.is_cuda: m = mask.unsqueeze(1).to(torch.float16) diff --git a/sam3/train/matcher.py b/sam3/train/matcher.py index d3e323e..5adc6a4 100644 --- a/sam3/train/matcher.py +++ b/sam3/train/matcher.py @@ -59,9 +59,9 @@ def __init__( self.cost_bbox = cost_bbox self.cost_giou = cost_giou self.norm = nn.Sigmoid() if focal_loss else nn.Softmax(-1) - assert ( - cost_class != 0 or cost_bbox != 0 or cost_giou != 0 - ), "all costs cant be 0" + assert cost_class != 0 or cost_bbox != 0 or cost_giou != 0, ( + "all costs cant be 0" + ) self.focal_loss = focal_loss self.focal_alpha = focal_alpha self.focal_gamma = focal_gamma @@ -196,9 +196,9 @@ def __init__( self.cost_bbox = cost_bbox self.cost_giou = cost_giou self.norm = nn.Sigmoid() - assert ( - cost_class != 0 or cost_bbox != 0 or cost_giou != 0 - ), "all costs cant be 0" + assert cost_class != 0 or cost_bbox != 0 or cost_giou != 0, ( + "all costs cant be 0" + ) @torch.no_grad() def forward(self, outputs, batched_targets, repeats=0, repeat_batch=1): @@ -321,9 +321,9 @@ def __init__( self.alpha = alpha self.gamma = gamma self.stable = stable - assert ( - cost_class != 0 or cost_bbox != 0 or cost_giou != 0 - ), "all costs cant be 0" + assert cost_class != 0 or cost_bbox != 0 or cost_giou != 0, ( + "all costs cant be 0" + ) @torch.no_grad() def forward(self, outputs, batched_targets, repeats=1, repeat_batch=1): @@ -469,9 +469,9 @@ def __init__( self.cost_bbox = cost_bbox self.cost_giou = cost_giou self.norm = nn.Sigmoid() - assert ( - cost_class != 0 or cost_bbox != 0 or cost_giou != 0 - ), "all costs cant be 0" + assert cost_class != 0 or cost_bbox != 0 or cost_giou != 0, ( + "all costs cant be 0" + ) self.focal = focal if focal: self.alpha = alpha diff --git a/sam3/train/optim/optimizer.py b/sam3/train/optim/optimizer.py index d0e7b75..66f305e 100644 --- a/sam3/train/optim/optimizer.py +++ b/sam3/train/optim/optimizer.py @@ -211,9 +211,9 @@ def unix_module_cls_pattern_to_parameter_names( "match any classes in the model" ) matching_parameters = module_cls_to_param_names[module_cls] - assert ( - len(matching_parameters) > 0 - ), f"module_cls_name {module_cls_name} does not contain any parameters in the model" + assert len(matching_parameters) > 0, ( + f"module_cls_name {module_cls_name} does not contain any parameters in the model" + ) logging.info( f"Matches for module_cls_name [{module_cls_name}]: {matching_parameters} " ) @@ -239,9 +239,9 @@ def unix_param_pattern_to_parameter_names( allowed_parameter_names = [] for param_name in filter_param_names: matching_parameters = set(fnmatch.filter(parameter_names, param_name)) - assert ( - len(matching_parameters) >= 1 - ), f"param_name {param_name} does not match any parameters in the model" + assert len(matching_parameters) >= 1, ( + f"param_name {param_name} does not match any parameters in the model" + ) logging.info(f"Matches for param_name [{param_name}]: {matching_parameters}") allowed_parameter_names.append(matching_parameters) return set.union(*allowed_parameter_names) diff --git a/sam3/train/train.py b/sam3/train/train.py index b192059..df6cbe1 100644 --- a/sam3/train/train.py +++ b/sam3/train/train.py @@ -209,9 +209,9 @@ def main(args) -> None: }, } if "include_nodes" in submitit_conf: - assert ( - len(submitit_conf["include_nodes"]) >= cfg.launcher.num_nodes - ), "Not enough nodes" + assert len(submitit_conf["include_nodes"]) >= cfg.launcher.num_nodes, ( + "Not enough nodes" + ) job_kwargs["slurm_additional_parameters"]["nodelist"] = " ".join( submitit_conf["include_nodes"] ) diff --git a/sam3/train/transforms/basic.py b/sam3/train/transforms/basic.py index b08dfae..7211389 100644 --- a/sam3/train/transforms/basic.py +++ b/sam3/train/transforms/basic.py @@ -276,9 +276,9 @@ def __call__(self, img: PIL.Image.Image, target: dict): max(0, minY - h + 1), max(maxY - 1, max(0, minY - h + 1)) ) result_img, result_target = crop(img, target, [j, i, h, w]) - assert ( - len(result_target["boxes"]) == init_boxes - ), f"img_w={img.width}\timg_h={img.height}\tminX={minX}\tminY={minY}\tmaxX={maxX}\tmaxY={maxY}\tminW={minW}\tminH={minH}\tmaxW={maxW}\tmaxH={maxH}\tw={w}\th={h}\ti={i}\tj={j}\tinit_boxes={init_boxes_tensor}\tresults={result_target['boxes']}" + assert len(result_target["boxes"]) == init_boxes, ( + f"img_w={img.width}\timg_h={img.height}\tminX={minX}\tminY={minY}\tmaxX={maxX}\tmaxY={maxY}\tminW={minW}\tminH={minH}\tmaxW={maxW}\tmaxH={maxH}\tw={w}\th={h}\ti={i}\tj={j}\tinit_boxes={init_boxes_tensor}\tresults={result_target['boxes']}" + ) return result_img, result_target else: diff --git a/sam3/train/transforms/filter_query_transforms.py b/sam3/train/transforms/filter_query_transforms.py index de66c93..3ebd64e 100644 --- a/sam3/train/transforms/filter_query_transforms.py +++ b/sam3/train/transforms/filter_query_transforms.py @@ -379,9 +379,9 @@ def __call__(self, datapoint, **kwargs): if len(new_find_queries) == 0: start_with_zero_check = True - assert ( - start_with_zero_check - ), "Invalid Find queries, they need to start at query_processing_order = 0" + assert start_with_zero_check, ( + "Invalid Find queries, they need to start at query_processing_order = 0" + ) datapoint.find_queries = new_find_queries diff --git a/sam3/train/transforms/point_sampling.py b/sam3/train/transforms/point_sampling.py index d27c6b8..521e2f5 100644 --- a/sam3/train/transforms/point_sampling.py +++ b/sam3/train/transforms/point_sampling.py @@ -249,9 +249,9 @@ def _get_original_box(self, target_object): def _get_target_object(self, datapoint, query): img = datapoint.images[query.image_id] targets = query.object_ids_output - assert ( - len(targets) == 1 - ), "Geometric queries only support a single target object." + assert len(targets) == 1, ( + "Geometric queries only support a single target object." + ) target_idx = targets[0] return img.objects[target_idx] diff --git a/sam3/train/utils/checkpoint_utils.py b/sam3/train/utils/checkpoint_utils.py index 465e006..32ca776 100644 --- a/sam3/train/utils/checkpoint_utils.py +++ b/sam3/train/utils/checkpoint_utils.py @@ -36,9 +36,9 @@ def unix_pattern_to_parameter_names( parameter_names = [] for param_name in constraints: matching_parameters = set(fnmatch.filter(all_parameter_names, param_name)) - assert ( - len(matching_parameters) > 0 - ), f"param_names {param_name} don't match any param in the given names." + assert len(matching_parameters) > 0, ( + f"param_names {param_name} don't match any param in the given names." + ) parameter_names.append(matching_parameters) return set.union(*parameter_names) diff --git a/sam3/train/utils/train_utils.py b/sam3/train/utils/train_utils.py index 27e9b93..ca259a3 100644 --- a/sam3/train/utils/train_utils.py +++ b/sam3/train/utils/train_utils.py @@ -82,9 +82,9 @@ def get_machine_local_and_dist_rank(): """ local_rank = int(os.environ.get("LOCAL_RANK", None)) distributed_rank = int(os.environ.get("RANK", None)) - assert ( - local_rank is not None and distributed_rank is not None - ), "Please the set the RANK and LOCAL_RANK environment variables." + assert local_rank is not None and distributed_rank is not None, ( + "Please the set the RANK and LOCAL_RANK environment variables." + ) return local_rank, distributed_rank diff --git a/sam3/visualization_utils.py b/sam3/visualization_utils.py index b39d8a4..c007c38 100644 --- a/sam3/visualization_utils.py +++ b/sam3/visualization_utils.py @@ -158,22 +158,22 @@ def plot_mask(mask, color="r", ax=None): def normalize_bbox(bbox_xywh, img_w, img_h): # Assumes bbox_xywh is in XYWH format if isinstance(bbox_xywh, list): - assert ( - len(bbox_xywh) == 4 - ), "bbox_xywh list must have 4 elements. Batching not support except for torch tensors." + assert len(bbox_xywh) == 4, ( + "bbox_xywh list must have 4 elements. Batching not support except for torch tensors." + ) normalized_bbox = bbox_xywh.copy() normalized_bbox[0] /= img_w normalized_bbox[1] /= img_h normalized_bbox[2] /= img_w normalized_bbox[3] /= img_h else: - assert isinstance( - bbox_xywh, torch.Tensor - ), "Only torch tensors are supported for batching." + assert isinstance(bbox_xywh, torch.Tensor), ( + "Only torch tensors are supported for batching." + ) normalized_bbox = bbox_xywh.clone() - assert ( - normalized_bbox.size(-1) == 4 - ), "bbox_xywh tensor must have last dimension of size 4." + assert normalized_bbox.size(-1) == 4, ( + "bbox_xywh tensor must have last dimension of size 4." + ) normalized_bbox[..., 0] /= img_w normalized_bbox[..., 1] /= img_h normalized_bbox[..., 2] /= img_w @@ -245,9 +245,9 @@ def visualize_formatted_frame_output( num_outputs = len(outputs_list) if titles is None: titles = [f"Set {i + 1}" for i in range(num_outputs)] - assert ( - len(titles) == num_outputs - ), "length of `titles` should match that of `outputs_list` if not None." + assert len(titles) == num_outputs, ( + "length of `titles` should match that of `outputs_list` if not None." + ) _, axes = plt.subplots(1, num_outputs, figsize=figsize) if num_outputs == 1: @@ -703,9 +703,9 @@ def get_all_annotations_for_frame( # Get the frame video_df_current = video_df[video_df.id == video_id] - assert ( - len(video_df_current) == 1 - ), f"Expected 1 video row, got {len(video_df_current)}" + assert len(video_df_current) == 1, ( + f"Expected 1 video row, got {len(video_df_current)}" + ) video_row = video_df_current.iloc[0] file_name = video_row.file_names[frame_idx] file_path = os.path.join( diff --git a/scripts/eval/silver/extract_frames.py b/scripts/eval/silver/extract_frames.py index 6be7f05..5c4285d 100644 --- a/scripts/eval/silver/extract_frames.py +++ b/scripts/eval/silver/extract_frames.py @@ -68,9 +68,9 @@ def process_image(args): def main(): assert len(sys.argv) > 1, "You have to provide the name of the dataset" dataset_name = sys.argv[1] - assert ( - dataset_name in annotation_files - ), f"The dataset can be one of {list(annotation_files.keys())}" + assert dataset_name in annotation_files, ( + f"The dataset can be one of {list(annotation_files.keys())}" + ) all_outputs = [] for file in annotation_files[dataset_name]: with open(os.path.join(config["path_annotations"], file), "r") as f: diff --git a/scripts/eval/veval/saco_yt1b_frame_prep_util.py b/scripts/eval/veval/saco_yt1b_frame_prep_util.py index 49c4f79..d4f14ac 100644 --- a/scripts/eval/veval/saco_yt1b_frame_prep_util.py +++ b/scripts/eval/veval/saco_yt1b_frame_prep_util.py @@ -58,9 +58,9 @@ def _get_yt_video_id_map_info(self): df = self.yt1b_start_end_time_df[ self.yt1b_start_end_time_df.saco_yt1b_id == self.saco_yt1b_id ] - assert ( - len(df) == 1 - ), f"Expected exactly 1 row for saco_yt1b_id: {self.saco_yt1b_id}, found {len(df)}" + assert len(df) == 1, ( + f"Expected exactly 1 row for saco_yt1b_id: {self.saco_yt1b_id}, found {len(df)}" + ) id_and_frame_map_row = df.iloc[0] yt_video_id = ( @@ -82,9 +82,9 @@ def _get_yt_video_id_map_info(self): def download_youtube_video(self): video_url = f"https://youtube.com/watch?v={self.yt_video_id}" - assert os.path.exists( - self.cookies_file - ), f"Cookies file '{self.cookies_file}' not found. Must have it to download videos." + assert os.path.exists(self.cookies_file), ( + f"Cookies file '{self.cookies_file}' not found. Must have it to download videos." + ) outtmpl = self.raw_video_path From e54adc4523ce04dd4ed47cd643a2639440f99d3a Mon Sep 17 00:00:00 2001 From: Nadav Yutal Date: Mon, 30 Mar 2026 03:13:57 -0700 Subject: [PATCH 19/31] Extract shared segmentation inputs into reusable component Summary: All three segment tracking demo pages duplicate the same input widgets (Everstore Handle, Text Queries, Confidence Threshold, Prediction Tier, and optionally Thumbnail Size). Extract them into a SegmentationInputs dataclass with a create() staticmethod that renders all shared Gradio widgets and returns them as a typed bundle. This eliminates duplication and ensures widget consistency across the Segmentation, Query Comparison, and Incremental Query pages. Differential Revision: D98651970 fbshipit-source-id: 60540a719b888095c5c8d0ba5e9bb321a4bd7cf1 --- sam3/model/sam3_image.py | 94 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/sam3/model/sam3_image.py b/sam3/model/sam3_image.py index ee13d2f..7a47ca1 100644 --- a/sam3/model/sam3_image.py +++ b/sam3/model/sam3_image.py @@ -495,6 +495,100 @@ def forward_grounding( self._compute_matching(out, self.back_convert(find_target)) return out + def forward_grounding_detect_only( + self, + backbone_out, + find_input, + geometric_prompt: Prompt, + ): + """Like forward_grounding but skips the segmentation head (no masks). + + Returns (out, grounding_state) where: + - out contains pred_logits, pred_boxes, pred_boxes_xyxy (no pred_masks) + - grounding_state has intermediate tensors for forward_segmentation_from_state() + """ + with torch.profiler.record_function("SAM3Image._encode_prompt"): + prompt, prompt_mask, backbone_out = self._encode_prompt( + backbone_out, find_input, geometric_prompt + ) + with torch.profiler.record_function("SAM3Image._run_encoder"): + backbone_out, encoder_out, _ = self._run_encoder( + backbone_out, find_input, prompt, prompt_mask + ) + out = { + "encoder_hidden_states": encoder_out["encoder_hidden_states"], + "prev_encoder_out": { + "encoder_out": encoder_out, + "backbone_out": backbone_out, + }, + } + with torch.profiler.record_function("SAM3Image._run_decoder"): + out, hs = self._run_decoder( + memory=out["encoder_hidden_states"], + pos_embed=encoder_out["pos_embed"], + src_mask=encoder_out["padding_mask"], + out=out, + prompt=prompt, + prompt_mask=prompt_mask, + encoder_out=encoder_out, + ) + + grounding_state = { + "hs": hs, + "backbone_out": backbone_out, + "img_ids": find_input.img_ids, + "vis_feat_sizes": encoder_out["vis_feat_sizes"], + "prompt": prompt, + "prompt_mask": prompt_mask, + } + return out, grounding_state + + def forward_segmentation_from_state( + self, + det_out, + grounding_state, + query_indices=None, + ): + """Run the segmentation (mask) head using state from detect-only pass. + + Args: + det_out: Detection output dict from forward_grounding_detect_only. + grounding_state: Intermediate state from forward_grounding_detect_only. + query_indices: Optional tensor of batch indices to generate masks for. + If None, generates masks for all queries. + + Returns: + seg_out dict with pred_masks for the selected queries. + """ + hs = grounding_state["hs"] + backbone_out = grounding_state["backbone_out"] + img_ids = grounding_state["img_ids"] + vis_feat_sizes = grounding_state["vis_feat_sizes"] + encoder_hidden_states = det_out["encoder_hidden_states"] + prompt = grounding_state["prompt"] + prompt_mask = grounding_state["prompt_mask"] + + if query_indices is not None: + hs = hs[:, query_indices] + img_ids = img_ids[query_indices] + encoder_hidden_states = encoder_hidden_states[:, query_indices] + prompt = prompt[:, query_indices] + prompt_mask = prompt_mask[query_indices] + + seg_out: Dict = {"encoder_hidden_states": encoder_hidden_states} + with torch.profiler.record_function("SAM3Image._run_segmentation_heads"): + self._run_segmentation_heads( + out=seg_out, + backbone_out=backbone_out, + img_ids=img_ids, + vis_feat_sizes=vis_feat_sizes, + encoder_hidden_states=encoder_hidden_states, + prompt=prompt, + prompt_mask=prompt_mask, + hs=hs, + ) + return seg_out + def _postprocess_out(self, out: Dict, multimask_output: bool = False): # For multimask output, during eval we return the single best mask with the dict keys expected by the evaluators, but also return the multimasks output with new keys. num_mask_boxes = out["pred_boxes"].size(1) From 29aecc4b7d52ba9c5794d53ce20598899de2048f Mon Sep 17 00:00:00 2001 From: Nadav Yutal Date: Tue, 31 Mar 2026 02:51:54 -0700 Subject: [PATCH 20/31] Revert unintended changes from D98651970 Summary: D98651970 accidentally included changes to sam3_image.py, shopping_segmentation_images config, inference_engine, and tests that were not part of the shared segmentation inputs refactor. This reverts only those unintended files. Reviewed By: omriharosh Differential Revision: D98875697 fbshipit-source-id: 3e47fa686364f645295ddff094a5eff2237e9b33 --- sam3/model/sam3_image.py | 94 ---------------------------------------- 1 file changed, 94 deletions(-) diff --git a/sam3/model/sam3_image.py b/sam3/model/sam3_image.py index 7a47ca1..ee13d2f 100644 --- a/sam3/model/sam3_image.py +++ b/sam3/model/sam3_image.py @@ -495,100 +495,6 @@ def forward_grounding( self._compute_matching(out, self.back_convert(find_target)) return out - def forward_grounding_detect_only( - self, - backbone_out, - find_input, - geometric_prompt: Prompt, - ): - """Like forward_grounding but skips the segmentation head (no masks). - - Returns (out, grounding_state) where: - - out contains pred_logits, pred_boxes, pred_boxes_xyxy (no pred_masks) - - grounding_state has intermediate tensors for forward_segmentation_from_state() - """ - with torch.profiler.record_function("SAM3Image._encode_prompt"): - prompt, prompt_mask, backbone_out = self._encode_prompt( - backbone_out, find_input, geometric_prompt - ) - with torch.profiler.record_function("SAM3Image._run_encoder"): - backbone_out, encoder_out, _ = self._run_encoder( - backbone_out, find_input, prompt, prompt_mask - ) - out = { - "encoder_hidden_states": encoder_out["encoder_hidden_states"], - "prev_encoder_out": { - "encoder_out": encoder_out, - "backbone_out": backbone_out, - }, - } - with torch.profiler.record_function("SAM3Image._run_decoder"): - out, hs = self._run_decoder( - memory=out["encoder_hidden_states"], - pos_embed=encoder_out["pos_embed"], - src_mask=encoder_out["padding_mask"], - out=out, - prompt=prompt, - prompt_mask=prompt_mask, - encoder_out=encoder_out, - ) - - grounding_state = { - "hs": hs, - "backbone_out": backbone_out, - "img_ids": find_input.img_ids, - "vis_feat_sizes": encoder_out["vis_feat_sizes"], - "prompt": prompt, - "prompt_mask": prompt_mask, - } - return out, grounding_state - - def forward_segmentation_from_state( - self, - det_out, - grounding_state, - query_indices=None, - ): - """Run the segmentation (mask) head using state from detect-only pass. - - Args: - det_out: Detection output dict from forward_grounding_detect_only. - grounding_state: Intermediate state from forward_grounding_detect_only. - query_indices: Optional tensor of batch indices to generate masks for. - If None, generates masks for all queries. - - Returns: - seg_out dict with pred_masks for the selected queries. - """ - hs = grounding_state["hs"] - backbone_out = grounding_state["backbone_out"] - img_ids = grounding_state["img_ids"] - vis_feat_sizes = grounding_state["vis_feat_sizes"] - encoder_hidden_states = det_out["encoder_hidden_states"] - prompt = grounding_state["prompt"] - prompt_mask = grounding_state["prompt_mask"] - - if query_indices is not None: - hs = hs[:, query_indices] - img_ids = img_ids[query_indices] - encoder_hidden_states = encoder_hidden_states[:, query_indices] - prompt = prompt[:, query_indices] - prompt_mask = prompt_mask[query_indices] - - seg_out: Dict = {"encoder_hidden_states": encoder_hidden_states} - with torch.profiler.record_function("SAM3Image._run_segmentation_heads"): - self._run_segmentation_heads( - out=seg_out, - backbone_out=backbone_out, - img_ids=img_ids, - vis_feat_sizes=vis_feat_sizes, - encoder_hidden_states=encoder_hidden_states, - prompt=prompt, - prompt_mask=prompt_mask, - hs=hs, - ) - return seg_out - def _postprocess_out(self, out: Dict, multimask_output: bool = False): # For multimask output, during eval we return the single best mask with the dict keys expected by the evaluators, but also return the multimasks output with new keys. num_mask_boxes = out["pred_boxes"].size(1) From bfbed072a07a6a52c8d5fdc75a7a186251a835b1 Mon Sep 17 00:00:00 2001 From: generatedunixname26232875646305581 Date: Tue, 31 Mar 2026 03:16:31 -0700 Subject: [PATCH 21/31] Daily `arc lint --take BLACK` Differential Revision: D98889675 fbshipit-source-id: e9d0089e5abb73b9327f5453ad20fa5060b12a40 --- sam3/model/video_tracking_multiplex_demo.py | 6 +-- sam3/train/data/coco_json_loaders.py | 6 +-- sam3/train/trainer.py | 42 ++++++++++----------- scripts/eval/silver/download_videos.py | 6 +-- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/sam3/model/video_tracking_multiplex_demo.py b/sam3/model/video_tracking_multiplex_demo.py index c572577..067907c 100644 --- a/sam3/model/video_tracking_multiplex_demo.py +++ b/sam3/model/video_tracking_multiplex_demo.py @@ -1734,9 +1734,9 @@ def add_new_masks( ) inference_state["multiplex_state"] = multiplex_state else: - assert ( - self.is_dynamic_model - ), "New objects are not allowed after state creation" + assert self.is_dynamic_model, ( + "New objects are not allowed after state creation" + ) for i in range(num_objects): mask_inputs_per_frame[i][frame_idx] = mask_inputs_video_res[i : i + 1] diff --git a/sam3/train/data/coco_json_loaders.py b/sam3/train/data/coco_json_loaders.py index 1618e19..d39b6f4 100644 --- a/sam3/train/data/coco_json_loaders.py +++ b/sam3/train/data/coco_json_loaders.py @@ -142,9 +142,9 @@ def __init__( self.prompts = {} for loc_dict in prompts: self.prompts[int(loc_dict["id"])] = loc_dict["name"] - assert len(self.prompts) == len( - self._sorted_cat_ids - ), "Number of prompts must match number of categories" + assert len(self.prompts) == len(self._sorted_cat_ids), ( + "Number of prompts must match number of categories" + ) def getDatapointIds(self): """Return all datapoint indices for training.""" diff --git a/sam3/train/trainer.py b/sam3/train/trainer.py index 5a37886..cc6c37e 100644 --- a/sam3/train/trainer.py +++ b/sam3/train/trainer.py @@ -209,9 +209,9 @@ def __init__( set_seeds(seed_value, self.max_epochs, self.distributed_rank) log_env_variables() - assert ( - is_dist_avail_and_initialized() - ), "Torch distributed needs to be initialized before calling the trainer." + assert is_dist_avail_and_initialized(), ( + "Torch distributed needs to be initialized before calling the trainer." + ) self._setup_components() # Except Optimizer everything is setup here. self._move_to_device() @@ -221,9 +221,9 @@ def __init__( self.time_elapsed_meter = DurationMeter("Time Elapsed", self.device, ":.2f") if self.checkpoint_conf.resume_from is not None: - assert os.path.exists( - self.checkpoint_conf.resume_from - ), f"The 'resume_from' checkpoint {self.checkpoint_conf.resume_from} does not exist!" + assert os.path.exists(self.checkpoint_conf.resume_from), ( + f"The 'resume_from' checkpoint {self.checkpoint_conf.resume_from} does not exist!" + ) dst = os.path.join(self.checkpoint_conf.save_dir, "checkpoint.pt") if self.distributed_rank == 0 and not os.path.exists(dst): # Copy the "resume_from" checkpoint to the checkpoint folder @@ -471,9 +471,9 @@ def _find_loss(self, key: str): return self.loss[key] assert key != "all", "Loss must be specified for key='all'" - assert ( - "default" in self.loss - ), f"Key {key} not found in losss, and no default provided" + assert "default" in self.loss, ( + f"Key {key} not found in losss, and no default provided" + ) return self.loss["default"] def _find_meter(self, phase: str, key: str): @@ -916,12 +916,12 @@ def _run_step( self.optim.zero_grad(set_to_none=True) if self.gradient_accumulation_steps > 1: - assert isinstance( - batch, list - ), f"Expected a list of batches, got {type(batch)}" - assert ( - len(batch) == self.gradient_accumulation_steps - ), f"Expected {self.gradient_accumulation_steps} batches, got {len(batch)}" + assert isinstance(batch, list), ( + f"Expected a list of batches, got {type(batch)}" + ) + assert len(batch) == self.gradient_accumulation_steps, ( + f"Expected {self.gradient_accumulation_steps} batches, got {len(batch)}" + ) accum_steps = len(batch) else: accum_steps = 1 @@ -1033,9 +1033,9 @@ def _reset_meters(self, phases: str) -> None: def _check_val_key_match(self, val_keys, phase): if val_keys is not None: # Check if there are any duplicates - assert len(val_keys) == len( - set(val_keys) - ), f"Duplicate keys in val datasets, keys: {val_keys}" + assert len(val_keys) == len(set(val_keys)), ( + f"Duplicate keys in val datasets, keys: {val_keys}" + ) # Check that the keys match the meter keys if self.meters_conf is not None and phase in self.meters_conf: @@ -1049,9 +1049,9 @@ def _check_val_key_match(self, val_keys, phase): loss_keys = set(self.loss_conf.keys()) - set(["all"]) if "default" not in loss_keys: for k in val_keys: - assert ( - k in loss_keys - ), f"Error: key {k} is not defined in the losses, and no default is set" + assert k in loss_keys, ( + f"Error: key {k} is not defined in the losses, and no default is set" + ) def _setup_components(self): # Get the keys for all the val datasets, if any diff --git a/scripts/eval/silver/download_videos.py b/scripts/eval/silver/download_videos.py index 9b6f873..d0a1ba8 100644 --- a/scripts/eval/silver/download_videos.py +++ b/scripts/eval/silver/download_videos.py @@ -243,9 +243,9 @@ def download_sav(): def main(): assert len(sys.argv) > 1, "You have to provide the name of the dataset" dataset_name = sys.argv[1] - assert ( - dataset_name in annotation_files - ), f"The dataset can be one of {list(annotation_files.keys())}" + assert dataset_name in annotation_files, ( + f"The dataset can be one of {list(annotation_files.keys())}" + ) if dataset_name == "yt1b": download_youtube() From 44ef224799e7b32e017855823883909d38ffb30f Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Sun, 12 Apr 2026 02:38:44 -0700 Subject: [PATCH 22/31] Fix SAM3 io_utils to handle extensionless video files from OIL Summary: Fix SAM3 io_utils.py to handle extensionless video files from OIL (Object Image Library). Previously, the code would immediately raise NotImplementedError when encountering files without recognized video extensions. This change attempts to load such files as videos using the video loader, only raising an exception if the actual decoding fails. This enables SAM3 to process OIL-stored video paths that don't include file extensions. Differential Revision: D99228861 fbshipit-source-id: c93d089ad4b7eac9e0eb629d7a71b021604db410 --- sam3/model/io_utils.py | 18 ++++++- test/test_io_utils.py | 109 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 test/test_io_utils.py diff --git a/sam3/model/io_utils.py b/sam3/model/io_utils.py index 470bb43..762a57e 100644 --- a/sam3/model/io_utils.py +++ b/sam3/model/io_utils.py @@ -159,7 +159,23 @@ def load_video_frames( video_loader_type=video_loader_type, ) else: - raise NotImplementedError("Only video files and image folders are supported") + # No recognized extension (e.g., extensionless OIL paths) — attempt video loading. + # Only raise if the loader itself fails to decode frames. + try: + return load_video_frames_from_video_file( + video_path=video_path, + image_size=image_size, + offload_video_to_cpu=offload_video_to_cpu, + img_mean=img_mean, + img_std=img_std, + async_loading_frames=async_loading_frames, + video_loader_type=video_loader_type, + ) + except Exception as e: + raise NotImplementedError( + f"Only video files and image folders are supported; " + f"failed to load '{video_path}' as video: {e}" + ) from e def load_video_frames_from_image_folder( diff --git a/test/test_io_utils.py b/test/test_io_utils.py new file mode 100644 index 0000000..415a59f --- /dev/null +++ b/test/test_io_utils.py @@ -0,0 +1,109 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved + +"""Tests for io_utils extensionless video file handling (D99228861).""" + +import os +import tempfile +import unittest +from unittest.mock import MagicMock, patch + +from sam3.model.io_utils import load_video_frames + + +class TestLoadVideoFramesRouting(unittest.TestCase): + """Test that load_video_frames routes paths correctly based on extension.""" + + @patch("sam3.model.io_utils.load_video_frames_from_video_file") + def test_mp4_extension_routes_to_video_loader(self, mock_load_video): + """Paths with .mp4 extension should route to load_video_frames_from_video_file.""" + mock_load_video.return_value = ("frames", 480, 640) + result = load_video_frames( + video_path="/tmp/test_video.mp4", + image_size=256, + offload_video_to_cpu=True, + ) + mock_load_video.assert_called_once() + self.assertEqual(result, ("frames", 480, 640)) + + @patch("sam3.model.io_utils.load_video_frames_from_video_file") + def test_mov_extension_routes_to_video_loader(self, mock_load_video): + """Paths with .mov extension should route to load_video_frames_from_video_file.""" + mock_load_video.return_value = ("frames", 480, 640) + load_video_frames( + video_path="/tmp/test_video.mov", + image_size=256, + offload_video_to_cpu=True, + ) + mock_load_video.assert_called_once() + + @patch("sam3.model.io_utils.load_video_frames_from_video_file") + def test_extensionless_oil_path_routes_to_video_loader(self, mock_load_video): + """Extensionless OIL paths should attempt video loading (D99228861 fix).""" + mock_load_video.return_value = ("frames", 480, 640) + result = load_video_frames( + video_path="oil://fb_permanent/abc123def456", + image_size=256, + offload_video_to_cpu=True, + ) + mock_load_video.assert_called_once() + self.assertEqual(result, ("frames", 480, 640)) + + @patch("sam3.model.io_utils.load_video_frames_from_video_file") + def test_extensionless_bare_hash_routes_to_video_loader(self, mock_load_video): + """Bare hash paths without extension should attempt video loading.""" + mock_load_video.return_value = ("frames", 480, 640) + result = load_video_frames( + video_path="/data/videos/abc123def456", + image_size=256, + offload_video_to_cpu=True, + ) + mock_load_video.assert_called_once() + self.assertEqual(result, ("frames", 480, 640)) + + @patch("sam3.model.io_utils.load_video_frames_from_video_file") + def test_extensionless_path_raises_on_decode_failure(self, mock_load_video): + """Extensionless path that fails to decode should raise NotImplementedError.""" + mock_load_video.side_effect = RuntimeError("Could not decode video") + with self.assertRaises(NotImplementedError) as ctx: + load_video_frames( + video_path="oil://fb_permanent/corrupted_file", + image_size=256, + offload_video_to_cpu=True, + ) + self.assertIn("failed to load", str(ctx.exception)) + self.assertIn("oil://fb_permanent/corrupted_file", str(ctx.exception)) + + @patch("sam3.model.io_utils.load_video_frames_from_image_folder") + def test_directory_routes_to_image_folder_loader(self, mock_load_folder): + """Directory paths should route to load_video_frames_from_image_folder.""" + mock_load_folder.return_value = ("frames", 480, 640) + with tempfile.TemporaryDirectory() as tmpdir: + load_video_frames( + video_path=tmpdir, + image_size=256, + offload_video_to_cpu=True, + ) + mock_load_folder.assert_called_once() + + def test_dummy_video_pattern(self): + """ pattern should return dummy frames.""" + frames, h, w = load_video_frames( + video_path="", + image_size=64, + offload_video_to_cpu=True, + ) + self.assertEqual(frames.shape[0], 5) # 5 frames + self.assertEqual(h, 480) + self.assertEqual(w, 640) + + @patch("sam3.model.io_utils.load_video_frames_from_video_file") + def test_unknown_extension_routes_to_video_loader(self, mock_load_video): + """Paths with unrecognized extensions should attempt video loading.""" + mock_load_video.return_value = ("frames", 480, 640) + result = load_video_frames( + video_path="/tmp/video.xyz", + image_size=256, + offload_video_to_cpu=True, + ) + mock_load_video.assert_called_once() + self.assertEqual(result, ("frames", 480, 640)) From 967fdd651f71ca14949122fed4c918a778ca9334 Mon Sep 17 00:00:00 2001 From: generatedunixname1228101065403401 Date: Thu, 16 Apr 2026 03:26:38 -0700 Subject: [PATCH 23/31] Fix Unused Import issue in fbcode/deeplearning/projects/sam3_release/test/test_io_utils.py Differential Revision: D101129326 fbshipit-source-id: 6a73103cd7ea482d89053768321b78ccb8319185 --- test/test_io_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_io_utils.py b/test/test_io_utils.py index 415a59f..9d5e955 100644 --- a/test/test_io_utils.py +++ b/test/test_io_utils.py @@ -2,7 +2,6 @@ """Tests for io_utils extensionless video file handling (D99228861).""" -import os import tempfile import unittest from unittest.mock import MagicMock, patch From 2e0009e23f0ad0fbcbd0488df893d30d5c8c2565 Mon Sep 17 00:00:00 2001 From: Quan Tran Date: Tue, 21 Apr 2026 22:27:16 -0700 Subject: [PATCH 24/31] v3 code Summary: better design between LLM and SAM3 due to the embedding issue of SAM3 Reviewed By: mchorton Differential Revision: D101818594 fbshipit-source-id: b51c36defc6ed4adae5406e8eba31280e05cac9a --- sam3/model/sam3_base_predictor.py | 3 +++ sam3/model/sam3_video_inference.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/sam3/model/sam3_base_predictor.py b/sam3/model/sam3_base_predictor.py index a02d213..08a15b8 100644 --- a/sam3/model/sam3_base_predictor.py +++ b/sam3/model/sam3_base_predictor.py @@ -46,6 +46,7 @@ def handle_request(self, request): resource_path=request["resource_path"], session_id=request.get("session_id", None), offload_video_to_cpu=request.get("offload_video_to_cpu", False), + offload_state_to_cpu=request.get("offload_state_to_cpu", False), ) elif request_type == "add_prompt": return self.add_prompt( @@ -107,11 +108,13 @@ def start_session( resource_path, session_id=None, offload_video_to_cpu=False, + offload_state_to_cpu=False, ): """Start a new inference session on a video directory or path.""" init_kwargs = dict( resource_path=resource_path, offload_video_to_cpu=offload_video_to_cpu, + offload_state_to_cpu=offload_state_to_cpu, ) if hasattr(self, "async_loading_frames"): init_kwargs["async_loading_frames"] = self.async_loading_frames diff --git a/sam3/model/sam3_video_inference.py b/sam3/model/sam3_video_inference.py index 18cd387..375d9ec 100644 --- a/sam3/model/sam3_video_inference.py +++ b/sam3/model/sam3_video_inference.py @@ -56,6 +56,7 @@ def init_state( self, resource_path, offload_video_to_cpu=False, + offload_state_to_cpu=False, async_loading_frames=False, video_loader_type="cv2", ): @@ -71,6 +72,7 @@ def init_state( ) inference_state = {} inference_state["image_size"] = self.image_size + inference_state["offload_state_to_cpu"] = offload_state_to_cpu inference_state["num_frames"] = len(images) # the original video height and width, used for resizing final output scores inference_state["orig_height"] = orig_height @@ -551,17 +553,14 @@ def _cache_frame_outputs( def _build_tracker_output( self, inference_state, frame_idx, refined_obj_id_to_mask=None ): - assert ( + if ( "cached_frame_outputs" in inference_state and frame_idx in inference_state["cached_frame_outputs"] - ), ( - "No cached outputs found. Ensure normal propagation has run first to populate the cache." - ) - cached_outputs = inference_state["cached_frame_outputs"][frame_idx] - - obj_id_to_mask = cached_outputs.copy() + ): + obj_id_to_mask = inference_state["cached_frame_outputs"][frame_idx].copy() + else: + obj_id_to_mask = {} - # Update with refined masks if provided if refined_obj_id_to_mask is not None: for obj_id, refined_mask in refined_obj_id_to_mask.items(): assert refined_mask is not None, ( @@ -990,6 +989,7 @@ def _init_new_tracker_state(self, inference_state): video_height=inference_state["orig_height"], video_width=inference_state["orig_width"], num_frames=inference_state["num_frames"], + offload_state_to_cpu=inference_state.get("offload_state_to_cpu", False), ) @torch.inference_mode() From 7567e8028cbe991b129fcb937b627a08929853a0 Mon Sep 17 00:00:00 2001 From: generatedunixname1401739305002972 Date: Fri, 24 Apr 2026 04:43:37 -0700 Subject: [PATCH 25/31] Fix PYRE_MISSING_ANNOTATIONS issues in fbcode/deeplearning/projects/sam3_release/test/test_io_utils.py Differential Revision: D102320245 fbshipit-source-id: c44cb4c6613356c6039140ccb0f56d92a1e84a2a --- test/test_io_utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/test_io_utils.py b/test/test_io_utils.py index 9d5e955..b661709 100644 --- a/test/test_io_utils.py +++ b/test/test_io_utils.py @@ -48,7 +48,9 @@ def test_extensionless_oil_path_routes_to_video_loader(self, mock_load_video): self.assertEqual(result, ("frames", 480, 640)) @patch("sam3.model.io_utils.load_video_frames_from_video_file") - def test_extensionless_bare_hash_routes_to_video_loader(self, mock_load_video): + def test_extensionless_bare_hash_routes_to_video_loader( + self, mock_load_video: MagicMock + ) -> None: """Bare hash paths without extension should attempt video loading.""" mock_load_video.return_value = ("frames", 480, 640) result = load_video_frames( @@ -96,7 +98,9 @@ def test_dummy_video_pattern(self): self.assertEqual(w, 640) @patch("sam3.model.io_utils.load_video_frames_from_video_file") - def test_unknown_extension_routes_to_video_loader(self, mock_load_video): + def test_unknown_extension_routes_to_video_loader( + self, mock_load_video: MagicMock + ) -> None: """Paths with unrecognized extensions should attempt video loading.""" mock_load_video.return_value = ("frames", 480, 640) result = load_video_frames( From c3a42ff67487eb489f74dce7a747477324734e44 Mon Sep 17 00:00:00 2001 From: Quan Tran Date: Fri, 24 Apr 2026 10:21:30 -0700 Subject: [PATCH 26/31] SAM3 multi-GPU session as default Summary: Default to multi-GPUs to avoid OOM. CPU offload seems to not helping as it still need to copy back into memory for tracking ___ overriding_review_checks_triggers_an_audit_and_retroactive_review Oncall Short Name: storytelling_ai_fsx_health Differential Revision: D102039480 fbshipit-source-id: c3540efb984382756ead223f16b66f5d87bbeb0b --- sam3/model/sam3_base_predictor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sam3/model/sam3_base_predictor.py b/sam3/model/sam3_base_predictor.py index 08a15b8..fdacda4 100644 --- a/sam3/model/sam3_base_predictor.py +++ b/sam3/model/sam3_base_predictor.py @@ -64,6 +64,7 @@ def handle_request(self, request): getattr(self, "default_output_prob_thresh", 0.5), ), obj_id=request.get("obj_id", None), + rel_coordinates=request.get("rel_coordinates", True), ) elif request_type == "remove_object": return self.remove_object( @@ -146,6 +147,7 @@ def add_prompt( clear_old_boxes: bool = True, output_prob_thresh: float = 0.5, obj_id: Optional[int] = None, + rel_coordinates: bool = True, ): """Add text, box and/or point prompt on a specific video frame.""" session = self._get_session(session_id) @@ -175,6 +177,7 @@ def add_prompt( box_labels=bounding_box_labels, clear_old_boxes=clear_old_boxes, output_prob_thresh=output_prob_thresh, + rel_coordinates=rel_coordinates, ) if obj_id is not None: kwargs["obj_id"] = obj_id @@ -187,7 +190,8 @@ def add_prompt( valid_params = set(sig.parameters.keys()) filtered_kwargs = {k: v for k, v in kwargs.items() if k in valid_params} - frame_idx, outputs = self.model.add_prompt(**filtered_kwargs) + with torch.autocast(device_type="cuda", dtype=torch.bfloat16): + frame_idx, outputs = self.model.add_prompt(**filtered_kwargs) return {"frame_index": frame_idx, "outputs": outputs} def remove_object( From e6f0eae47abc794020b2927d227971a2c837d2fe Mon Sep 17 00:00:00 2001 From: generatedunixname1401739305002972 Date: Sat, 25 Apr 2026 22:30:00 -0700 Subject: [PATCH 27/31] Fix PYRE_MISSING_ANNOTATIONS issues in fbcode/deeplearning/projects/sam3_release/test/test_io_utils.py Differential Revision: D102459561 fbshipit-source-id: 7884c3f6ff6f06160170202b47c501b85f77e993 --- test/test_io_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_io_utils.py b/test/test_io_utils.py index b661709..0b5ab5b 100644 --- a/test/test_io_utils.py +++ b/test/test_io_utils.py @@ -25,7 +25,9 @@ def test_mp4_extension_routes_to_video_loader(self, mock_load_video): self.assertEqual(result, ("frames", 480, 640)) @patch("sam3.model.io_utils.load_video_frames_from_video_file") - def test_mov_extension_routes_to_video_loader(self, mock_load_video): + def test_mov_extension_routes_to_video_loader( + self, mock_load_video: MagicMock + ) -> None: """Paths with .mov extension should route to load_video_frames_from_video_file.""" mock_load_video.return_value = ("frames", 480, 640) load_video_frames( From 875ed6fa5af25f80e424a2e737940a65e13e8cbf Mon Sep 17 00:00:00 2001 From: generatedunixname1401739305002972 Date: Sun, 26 Apr 2026 03:15:30 -0700 Subject: [PATCH 28/31] Fix PYRE_MISSING_ANNOTATIONS issues in fbcode/deeplearning/projects/sam3_release/test/test_io_utils.py Differential Revision: D102507703 fbshipit-source-id: 77d76f5f50614b55a74b134e132d448368a3a97d --- test/test_io_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_io_utils.py b/test/test_io_utils.py index 0b5ab5b..bfa54a4 100644 --- a/test/test_io_utils.py +++ b/test/test_io_utils.py @@ -77,7 +77,9 @@ def test_extensionless_path_raises_on_decode_failure(self, mock_load_video): self.assertIn("oil://fb_permanent/corrupted_file", str(ctx.exception)) @patch("sam3.model.io_utils.load_video_frames_from_image_folder") - def test_directory_routes_to_image_folder_loader(self, mock_load_folder): + def test_directory_routes_to_image_folder_loader( + self, mock_load_folder: MagicMock + ) -> None: """Directory paths should route to load_video_frames_from_image_folder.""" mock_load_folder.return_value = ("frames", 480, 640) with tempfile.TemporaryDirectory() as tmpdir: From eef9c1e86a7639ab8f9e8434057a0e66abf94d96 Mon Sep 17 00:00:00 2001 From: generatedunixname1401739305002972 Date: Mon, 27 Apr 2026 10:02:50 -0700 Subject: [PATCH 29/31] Fix PYRE_MISSING_ANNOTATIONS issues in fbcode/deeplearning/projects/sam3_release/sam3/model/io_utils.py Differential Revision: D102576637 fbshipit-source-id: 76378a87b33ee16a736b79c32e312751a69913f0 --- sam3/model/io_utils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sam3/model/io_utils.py b/sam3/model/io_utils.py index 762a57e..49eedfd 100644 --- a/sam3/model/io_utils.py +++ b/sam3/model/io_utils.py @@ -7,7 +7,9 @@ import queue import re import time +import types from threading import Condition, get_ident, Lock, Thread +from typing import Optional import numpy as np import torch @@ -492,7 +494,7 @@ def __init__(self): self._waiters = queue.Queue() self._condition = Condition() - def acquire(self): + def acquire(self) -> None: ident = get_ident() with self._condition: self._waiters.put(ident) @@ -511,7 +513,12 @@ def release(self): def __enter__(self): self.acquire() - def __exit__(self, t, v, tb): + def __exit__( + self, + t: Optional[type[BaseException]], + v: Optional[BaseException], + tb: Optional[types.TracebackType], + ) -> None: self.release() From c97c893969003d3e6803fd5d679f21e515aef5ce Mon Sep 17 00:00:00 2001 From: generatedunixname1401739305002972 Date: Mon, 27 Apr 2026 12:38:24 -0700 Subject: [PATCH 30/31] Fix PYRE_MISSING_ANNOTATIONS issues in fbcode/deeplearning/projects/sam3_release/test/test_io_utils.py Differential Revision: D102578609 fbshipit-source-id: bfefceaad3e29999d3c73bc30b2508dafa5c60a4 --- test/test_io_utils.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/test_io_utils.py b/test/test_io_utils.py index bfa54a4..780b8af 100644 --- a/test/test_io_utils.py +++ b/test/test_io_utils.py @@ -13,7 +13,9 @@ class TestLoadVideoFramesRouting(unittest.TestCase): """Test that load_video_frames routes paths correctly based on extension.""" @patch("sam3.model.io_utils.load_video_frames_from_video_file") - def test_mp4_extension_routes_to_video_loader(self, mock_load_video): + def test_mp4_extension_routes_to_video_loader( + self, mock_load_video: MagicMock + ) -> None: """Paths with .mp4 extension should route to load_video_frames_from_video_file.""" mock_load_video.return_value = ("frames", 480, 640) result = load_video_frames( @@ -38,7 +40,9 @@ def test_mov_extension_routes_to_video_loader( mock_load_video.assert_called_once() @patch("sam3.model.io_utils.load_video_frames_from_video_file") - def test_extensionless_oil_path_routes_to_video_loader(self, mock_load_video): + def test_extensionless_oil_path_routes_to_video_loader( + self, mock_load_video: MagicMock + ) -> None: """Extensionless OIL paths should attempt video loading (D99228861 fix).""" mock_load_video.return_value = ("frames", 480, 640) result = load_video_frames( @@ -64,7 +68,9 @@ def test_extensionless_bare_hash_routes_to_video_loader( self.assertEqual(result, ("frames", 480, 640)) @patch("sam3.model.io_utils.load_video_frames_from_video_file") - def test_extensionless_path_raises_on_decode_failure(self, mock_load_video): + def test_extensionless_path_raises_on_decode_failure( + self, mock_load_video: MagicMock + ) -> None: """Extensionless path that fails to decode should raise NotImplementedError.""" mock_load_video.side_effect = RuntimeError("Could not decode video") with self.assertRaises(NotImplementedError) as ctx: @@ -90,7 +96,7 @@ def test_directory_routes_to_image_folder_loader( ) mock_load_folder.assert_called_once() - def test_dummy_video_pattern(self): + def test_dummy_video_pattern(self) -> None: """ pattern should return dummy frames.""" frames, h, w = load_video_frames( video_path="", From 3bb7a1eb89774cd834120e0ee06ddba34d4f19c6 Mon Sep 17 00:00:00 2001 From: Ryan Avery Date: Wed, 29 Apr 2026 15:22:44 -0700 Subject: [PATCH 31/31] Bump torch to 2.11 and torchvision to 0.26 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add explicit torch>=2.11,<2.12 and torchvision>=0.26,<0.27 constraints in pyproject.toml so the resolver pins the runtime stack instead of taking whatever timm/torchmetrics happens to pull. - Drop Python 3.9 from requires-python — torch 2.11 wheels start at cp310. - Regenerate uv.lock; CUDA runtime libs also flip from cu12 to cu13 because torch 2.11 PyPI wheels ship with CUDA 13. Users on cu126 should install from the PyTorch wheel index directly. --- pyproject.toml | 4 +- uv.lock | 2621 +++++++----------------------------------------- 2 files changed, 361 insertions(+), 2264 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4bb315a..cdfafa0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "sam3" dynamic = ["version"] description = "SAM3 (Segment Anything Model 3) implementation" readme = "README.md" -requires-python = ">=3.9,<3.13" +requires-python = ">=3.10,<3.13" license = {file = "LICENSE"} authors = [ {name = "Meta AI Research"} @@ -35,6 +35,8 @@ dependencies = [ "huggingface_hub", "einops", "psutil", + "torch>=2.11,<2.12", + "torchvision>=0.26,<0.27", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 3b681d5..5cd9d5a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.9, <3.13" +requires-python = ">=3.10, <3.13" resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'darwin'", "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", @@ -12,47 +12,15 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] - -[[package]] -name = "absl-py" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/10/2a/c93173ffa1b39c1d0395b7e842bbdc62e556ca9d8d3b5572926f3e4ca752/absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9", size = 116588, upload-time = "2025-07-03T09:31:44.05Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" }, + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] [[package]] name = "absl-py" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, @@ -147,44 +115,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] -[[package]] -name = "async-lru" -version = "2.0.5" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b2/4d/71ec4d3939dc755264f680f6c2b4906423a304c3d18e96853f0a595dfe97/async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb", size = 10380, upload-time = "2025-03-16T17:25:36.919Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069, upload-time = "2025-03-16T17:25:35.422Z" }, -] - [[package]] name = "async-lru" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/c3/bbf34f15ea88dfb649ab2c40f9d75081784a50573a9ea431563cab64adb8/async_lru-2.1.0.tar.gz", hash = "sha256:9eeb2fecd3fe42cc8a787fc32ead53a3a7158cc43d039c3c55ab3e4e5b2a80ed", size = 12041, upload-time = "2026-01-17T22:52:18.931Z" } wheels = [ @@ -227,13 +163,11 @@ name = "black" version = "24.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "mypy-extensions" }, { name = "packaging" }, { name = "pathspec" }, - { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] @@ -251,56 +185,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/62/6437212cf40e40b74dbc7e134700a21cb21a9ac7e46ade940b5d4826456f/black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9", size = 1417360, upload-time = "2024-02-12T20:34:56.41Z" }, { url = "https://files.pythonhosted.org/packages/36/8f/de0d339ae683422a8e15d6f74b8022d4947009c347d8c2178c303c68cc4d/black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693", size = 1739406, upload-time = "2024-02-12T20:23:59.596Z" }, { url = "https://files.pythonhosted.org/packages/3e/58/89e5f5a1c4c5b66dc74eabe6337623d53b4d1c27fbbbe16defee53397f60/black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982", size = 1373310, upload-time = "2024-02-12T20:25:27.243Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/eb61bca6e1bb69529bc1295f17ca7a0ebadf56c480de37b2acb485dc04e2/black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8", size = 1575445, upload-time = "2024-02-12T20:41:38.354Z" }, - { url = "https://files.pythonhosted.org/packages/1f/44/41f6766ad9007b98478547ba5cceada721383ae7a81793c1180cb7ad8b30/black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8", size = 1422403, upload-time = "2024-02-12T20:37:16.005Z" }, - { url = "https://files.pythonhosted.org/packages/d4/76/0c4bbb734126cdbbd7e82f6df4b58e67e17742250b15f1bac6901f1e8d9b/black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540", size = 1728526, upload-time = "2024-02-12T20:23:42.22Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/7441c2d961140a2273a4047429b4cd30b663d76c8c375d66f7fd57ae10f5/black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31", size = 1354913, upload-time = "2024-02-12T20:24:53.441Z" }, { url = "https://files.pythonhosted.org/packages/47/15/b3770bc3328685a53bc9c041136240146c5cd866a1f020c2cf47f2ff9683/black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6", size = 200610, upload-time = "2024-02-12T20:21:17.657Z" }, ] -[[package]] -name = "bleach" -version = "6.2.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "webencodings", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083, upload-time = "2024-10-29T18:30:40.477Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406, upload-time = "2024-10-29T18:30:38.186Z" }, -] - -[package.optional-dependencies] -css = [ - { name = "tinycss2", marker = "python_full_version < '3.10'" }, -] - [[package]] name = "bleach" version = "6.3.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "webencodings", marker = "python_full_version >= '3.10'" }, + { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } wheels = [ @@ -309,7 +202,7 @@ wheels = [ [package.optional-dependencies] css = [ - { name = "tinycss2", marker = "python_full_version >= '3.10'" }, + { name = "tinycss2" }, ] [[package]] @@ -326,8 +219,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, - { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -368,18 +260,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, - { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, - { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, - { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, - { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, - { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, - { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, - { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, ] [[package]] @@ -436,63 +316,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, - { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, - { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, - { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, - { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, - { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, - { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, - { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, - { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, - { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, - { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, - { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, -] - [[package]] name = "click" version = "8.3.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ @@ -526,79 +358,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, ] -[[package]] -name = "contourpy" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370, upload-time = "2024-08-27T21:00:03.328Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366, upload-time = "2024-08-27T20:50:09.947Z" }, - { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226, upload-time = "2024-08-27T20:50:16.1Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460, upload-time = "2024-08-27T20:50:22.536Z" }, - { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623, upload-time = "2024-08-27T20:50:28.806Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761, upload-time = "2024-08-27T20:50:35.126Z" }, - { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015, upload-time = "2024-08-27T20:50:40.318Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672, upload-time = "2024-08-27T20:50:55.643Z" }, - { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688, upload-time = "2024-08-27T20:51:11.293Z" }, - { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145, upload-time = "2024-08-27T20:51:15.2Z" }, - { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019, upload-time = "2024-08-27T20:51:19.365Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356, upload-time = "2024-08-27T20:51:24.146Z" }, - { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915, upload-time = "2024-08-27T20:51:28.683Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443, upload-time = "2024-08-27T20:51:33.675Z" }, - { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548, upload-time = "2024-08-27T20:51:39.322Z" }, - { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118, upload-time = "2024-08-27T20:51:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162, upload-time = "2024-08-27T20:51:49.683Z" }, - { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396, upload-time = "2024-08-27T20:52:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297, upload-time = "2024-08-27T20:52:21.843Z" }, - { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808, upload-time = "2024-08-27T20:52:25.163Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181, upload-time = "2024-08-27T20:52:29.13Z" }, - { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838, upload-time = "2024-08-27T20:52:33.911Z" }, - { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549, upload-time = "2024-08-27T20:52:39.179Z" }, - { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177, upload-time = "2024-08-27T20:52:44.789Z" }, - { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735, upload-time = "2024-08-27T20:52:51.05Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679, upload-time = "2024-08-27T20:52:58.473Z" }, - { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549, upload-time = "2024-08-27T20:53:06.593Z" }, - { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068, upload-time = "2024-08-27T20:53:23.442Z" }, - { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833, upload-time = "2024-08-27T20:53:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681, upload-time = "2024-08-27T20:53:43.05Z" }, - { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283, upload-time = "2024-08-27T20:53:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518, upload-time = "2024-08-27T20:56:01.333Z" }, - { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350, upload-time = "2024-08-27T20:56:05.432Z" }, - { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167, upload-time = "2024-08-27T20:56:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279, upload-time = "2024-08-27T20:56:15.41Z" }, - { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519, upload-time = "2024-08-27T20:56:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922, upload-time = "2024-08-27T20:56:26.983Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017, upload-time = "2024-08-27T20:56:42.246Z" }, - { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773, upload-time = "2024-08-27T20:56:58.58Z" }, - { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353, upload-time = "2024-08-27T20:57:02.718Z" }, - { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817, upload-time = "2024-08-27T20:57:06.328Z" }, - { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886, upload-time = "2024-08-27T20:57:10.863Z" }, - { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008, upload-time = "2024-08-27T20:57:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690, upload-time = "2024-08-27T20:57:19.321Z" }, - { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894, upload-time = "2024-08-27T20:57:23.873Z" }, - { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099, upload-time = "2024-08-27T20:57:28.58Z" }, - { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838, upload-time = "2024-08-27T20:57:32.913Z" }, -] - [[package]] name = "contourpy" version = "1.3.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "numpy", marker = "python_full_version == '3.10.*'" }, + { name = "numpy", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -690,94 +460,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] -[[package]] -name = "coverage" -version = "7.10.7" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, - { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, - { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, - { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, - { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, - { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, - { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, - { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, - { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, - { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, - { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, - { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, - { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, - { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, - { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, - { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, - { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, - { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, - { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, - { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, - { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, - { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, - { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, - { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, - { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, - { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, - { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, - { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, - { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version < '3.10'" }, -] - [[package]] name = "coverage" version = "7.13.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" }, @@ -823,21 +509,23 @@ wheels = [ [package.optional-dependencies] toml = [ - { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, + { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "cuda-bindings" -version = "12.9.4" +version = "13.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "(python_full_version >= '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "cuda-pathfinder" }, ] 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/53/1d/f7f2bcffe788aebd4325a34d8a976b219a0751c06707aa89c9e70355ceae/cuda_bindings-12.9.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9866ceec83e39337d1a1d64837864c964ad902992478caa288a0bc1be95f21aa", size = 12152579, upload-time = "2025-10-21T14:52:16.731Z" }, + { url = "https://files.pythonhosted.org/packages/1a/fe/7351d7e586a8b4c9f89731bfe4cf0148223e8f9903ff09571f78b3fb0682/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b395f79cb89ce0cd8effff07c4a1e20101b873c256a1aeb286e8fd7bd0f556", size = 5744254, upload-time = "2026-03-11T00:12:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ef/184aa775e970fc089942cd9ec6302e6e44679d4c14549c6a7ea45bf7f798/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6f3682ec3c4769326aafc67c2ba669d97d688d0b7e63e659d36d2f8b72f32d6", size = 6329075, upload-time = "2026-03-11T00:12:32.319Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, ] [[package]] @@ -848,6 +536,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/02/4dbe7568a42e46582248942f54dc64ad094769532adbe21e525e4edf7bc4/cuda_pathfinder-1.3.3-py3-none-any.whl", hash = "sha256:9984b664e404f7c134954a771be8775dfd6180ea1e1aef4a5a37d4be05d9bbb1", size = 27154, upload-time = "2025-12-04T22:35:08.996Z" }, ] +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -875,10 +606,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6b/668f21567e3250463beb6a401e7d598baa2a0907224000d7f68b9442c243/debugpy-1.8.20-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:bff8990f040dacb4c314864da95f7168c5a58a30a66e0eea0fb85e2586a92cd6", size = 2100484, upload-time = "2026-01-29T23:04:09.929Z" }, - { url = "https://files.pythonhosted.org/packages/cf/49/223143d1da586b891f35a45515f152742ad85bfc10d2e02e697f65c83b32/debugpy-1.8.20-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:70ad9ae09b98ac307b82c16c151d27ee9d68ae007a2e7843ba621b5ce65333b5", size = 3081272, upload-time = "2026-01-29T23:04:11.664Z" }, - { url = "https://files.pythonhosted.org/packages/b1/24/9f219c9290fe8bee4f63f9af8ebac440c802e6181d7f39a79abcb5fdff2f/debugpy-1.8.20-cp39-cp39-win32.whl", hash = "sha256:9eeed9f953f9a23850c85d440bf51e3c56ed5d25f8560eeb29add815bd32f7ee", size = 5285196, upload-time = "2026-01-29T23:04:13.105Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f3/4a12d7b1b09e3b79ba6e3edfa0c677b8b8bdf110bc4b3607e0f29fb4e8b3/debugpy-1.8.20-cp39-cp39-win_amd64.whl", hash = "sha256:760813b4fff517c75bfe7923033c107104e76acfef7bda011ffea8736e9a66f8", size = 5317163, upload-time = "2026-01-29T23:04:15.264Z" }, { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, ] @@ -948,8 +675,7 @@ version = "0.4.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, - { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "torch" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c1/08/b3334d7b543ac10dcb129cef4f84723ab696725512f18d69ab3a784b0bf5/fairscale-0.4.13.tar.gz", hash = "sha256:1b797825c427f5dba92253fd0d8daa574e8bd651a2423497775fab1b30cfb768", size = 266261, upload-time = "2022-12-11T18:09:16.892Z" } @@ -962,109 +688,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, ] -[[package]] -name = "filelock" -version = "3.19.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, -] - [[package]] name = "filelock" version = "3.20.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] -[[package]] -name = "fonttools" -version = "4.60.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/c4/db6a7b5eb0656534c3aa2596c2c5e18830d74f1b9aa5aa8a7dff63a0b11d/fonttools-4.60.2.tar.gz", hash = "sha256:d29552e6b155ebfc685b0aecf8d429cb76c14ab734c22ef5d3dea6fdf800c92c", size = 3562254, upload-time = "2025-12-09T13:38:11.835Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/de/9e10a99fb3070accb8884886a41a4ce54e49bf2fa4fc63f48a6cf2061713/fonttools-4.60.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e36fadcf7e8ca6e34d490eef86ed638d6fd9c55d2f514b05687622cfc4a7050", size = 2850403, upload-time = "2025-12-09T13:35:53.14Z" }, - { url = "https://files.pythonhosted.org/packages/e4/40/d5b369d1073b134f600a94a287e13b5bdea2191ba6347d813fa3da00e94a/fonttools-4.60.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e500fc9c04bee749ceabfc20cb4903f6981c2139050d85720ea7ada61b75d5c", size = 2398629, upload-time = "2025-12-09T13:35:56.471Z" }, - { url = "https://files.pythonhosted.org/packages/7c/b5/123819369aaf99d1e4dc49f1de1925d4edc7379114d15a56a7dd2e9d56e6/fonttools-4.60.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22efea5e784e1d1cd8d7b856c198e360a979383ebc6dea4604743b56da1cbc34", size = 4893471, upload-time = "2025-12-09T13:35:58.927Z" }, - { url = "https://files.pythonhosted.org/packages/24/29/f8f8acccb9716b899be4be45e9ce770d6aa76327573863e68448183091b0/fonttools-4.60.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:677aa92d84d335e4d301d8ba04afca6f575316bc647b6782cb0921943fcb6343", size = 4854686, upload-time = "2025-12-09T13:36:01.767Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0d/f3f51d7519f44f2dd5c9a60d7cd41185ebcee4348f073e515a3a93af15ff/fonttools-4.60.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:edd49d3defbf35476e78b61ff737ff5efea811acff68d44233a95a5a48252334", size = 4871233, upload-time = "2025-12-09T13:36:06.094Z" }, - { url = "https://files.pythonhosted.org/packages/cc/3f/4d4fd47d3bc40ab4d76718555185f8adffb5602ea572eac4bbf200c47d22/fonttools-4.60.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:126839492b69cecc5baf2bddcde60caab2ffafd867bbae2a88463fce6078ca3a", size = 4988936, upload-time = "2025-12-09T13:36:08.42Z" }, - { url = "https://files.pythonhosted.org/packages/01/6f/83bbdefa43f2c3ae206fd8c4b9a481f3c913eef871b1ce9a453069239e39/fonttools-4.60.2-cp310-cp310-win32.whl", hash = "sha256:ffcab6f5537136046ca902ed2491ab081ba271b07591b916289b7c27ff845f96", size = 2278044, upload-time = "2025-12-09T13:36:10.641Z" }, - { url = "https://files.pythonhosted.org/packages/d4/04/7d9a137e919d6c9ef26704b7f7b2580d9cfc5139597588227aacebc0e3b7/fonttools-4.60.2-cp310-cp310-win_amd64.whl", hash = "sha256:9c68b287c7ffcd29dd83b5f961004b2a54a862a88825d52ea219c6220309ba45", size = 2326522, upload-time = "2025-12-09T13:36:12.981Z" }, - { url = "https://files.pythonhosted.org/packages/e0/80/b7693d37c02417e162cc83cdd0b19a4f58be82c638b5d4ce4de2dae050c4/fonttools-4.60.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2aed0a7931401b3875265717a24c726f87ecfedbb7b3426c2ca4d2812e281ae", size = 2847809, upload-time = "2025-12-09T13:36:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/f9/9a/9c2c13bf8a6496ac21607d704e74e9cc68ebf23892cf924c9a8b5c7566b9/fonttools-4.60.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea6868e9d2b816c9076cfea77754686f3c19149873bdbc5acde437631c15df1", size = 2397302, upload-time = "2025-12-09T13:36:17.151Z" }, - { url = "https://files.pythonhosted.org/packages/56/f6/ce38ff6b2d2d58f6fd981d32f3942365bfa30eadf2b47d93b2d48bf6097f/fonttools-4.60.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fa27f34950aa1fe0f0b1abe25eed04770a3b3b34ad94e5ace82cc341589678a", size = 5054418, upload-time = "2025-12-09T13:36:19.062Z" }, - { url = "https://files.pythonhosted.org/packages/88/06/5353bea128ff39e857c31de3dd605725b4add956badae0b31bc9a50d4c8e/fonttools-4.60.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13a53d479d187b09bfaa4a35ffcbc334fc494ff355f0a587386099cb66674f1e", size = 5031652, upload-time = "2025-12-09T13:36:21.206Z" }, - { url = "https://files.pythonhosted.org/packages/71/05/ebca836437f6ebd57edd6428e7eff584e683ff0556ddb17d62e3b731f46c/fonttools-4.60.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fac5e921d3bd0ca3bb8517dced2784f0742bc8ca28579a68b139f04ea323a779", size = 5030321, upload-time = "2025-12-09T13:36:23.515Z" }, - { url = "https://files.pythonhosted.org/packages/57/f9/eb9d2a2ce30c99f840c1cc3940729a970923cf39d770caf88909d98d516b/fonttools-4.60.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:648f4f9186fd7f1f3cd57dbf00d67a583720d5011feca67a5e88b3a491952cfb", size = 5154255, upload-time = "2025-12-09T13:36:25.879Z" }, - { url = "https://files.pythonhosted.org/packages/08/a2/088b6ceba8272a9abb629d3c08f9c1e35e5ce42db0ccfe0c1f9f03e60d1d/fonttools-4.60.2-cp311-cp311-win32.whl", hash = "sha256:3274e15fad871bead5453d5ce02658f6d0c7bc7e7021e2a5b8b04e2f9e40da1a", size = 2276300, upload-time = "2025-12-09T13:36:27.772Z" }, - { url = "https://files.pythonhosted.org/packages/de/2f/8e4c3d908cc5dade7bb1316ce48589f6a24460c1056fd4b8db51f1fa309a/fonttools-4.60.2-cp311-cp311-win_amd64.whl", hash = "sha256:91d058d5a483a1525b367803abb69de0923fbd45e1f82ebd000f5c8aa65bc78e", size = 2327574, upload-time = "2025-12-09T13:36:30.89Z" }, - { url = "https://files.pythonhosted.org/packages/c0/30/530c9eddcd1c39219dc0aaede2b5a4c8ab80e0bb88d1b3ffc12944c4aac3/fonttools-4.60.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e0164b7609d2b5c5dd4e044b8085b7bd7ca7363ef8c269a4ab5b5d4885a426b2", size = 2847196, upload-time = "2025-12-09T13:36:33.262Z" }, - { url = "https://files.pythonhosted.org/packages/19/2f/4077a482836d5bbe3bc9dac1c004d02ee227cf04ed62b0a2dfc41d4f0dfd/fonttools-4.60.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1dd3d9574fc595c1e97faccae0f264dc88784ddf7fbf54c939528378bacc0033", size = 2395842, upload-time = "2025-12-09T13:36:35.47Z" }, - { url = "https://files.pythonhosted.org/packages/dd/05/aae5bb99c5398f8ed4a8b784f023fd9dd3568f0bd5d5b21e35b282550f11/fonttools-4.60.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98d0719f1b11c2817307d2da2e94296a3b2a3503f8d6252a101dca3ee663b917", size = 4949713, upload-time = "2025-12-09T13:36:37.874Z" }, - { url = "https://files.pythonhosted.org/packages/b4/37/49067349fc78ff0efbf09fadefe80ddf41473ca8f8a25400e3770da38328/fonttools-4.60.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d3ea26957dd07209f207b4fff64c702efe5496de153a54d3b91007ec28904dd", size = 4999907, upload-time = "2025-12-09T13:36:39.853Z" }, - { url = "https://files.pythonhosted.org/packages/16/31/d0f11c758bd0db36b664c92a0f9dfdcc2d7313749aa7d6629805c6946f21/fonttools-4.60.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ee301273b0850f3a515299f212898f37421f42ff9adfc341702582ca5073c13", size = 4939717, upload-time = "2025-12-09T13:36:43.075Z" }, - { url = "https://files.pythonhosted.org/packages/d9/bc/1cff0d69522e561bf1b99bee7c3911c08c25e919584827c3454a64651ce9/fonttools-4.60.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6eb4694cc3b9c03b7c01d65a9cf35b577f21aa6abdbeeb08d3114b842a58153", size = 5089205, upload-time = "2025-12-09T13:36:45.468Z" }, - { url = "https://files.pythonhosted.org/packages/05/e6/fb174f0069b7122e19828c551298bfd34fdf9480535d2a6ac2ed37afacd3/fonttools-4.60.2-cp312-cp312-win32.whl", hash = "sha256:57f07b616c69c244cc1a5a51072eeef07dddda5ebef9ca5c6e9cf6d59ae65b70", size = 2264674, upload-time = "2025-12-09T13:36:49.238Z" }, - { url = "https://files.pythonhosted.org/packages/75/57/6552ffd6b582d3e6a9f01780c5275e6dfff1e70ca146101733aa1c12a129/fonttools-4.60.2-cp312-cp312-win_amd64.whl", hash = "sha256:310035802392f1fe5a7cf43d76f6ff4a24c919e4c72c0352e7b8176e2584b8a0", size = 2314701, upload-time = "2025-12-09T13:36:51.09Z" }, - { url = "https://files.pythonhosted.org/packages/55/ae/a6d9446cb258d3fe87e311c2d7bacf8e8da3e5809fbdc3a8306db4f6b14e/fonttools-4.60.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a3c75b8b42f7f93906bdba9eb1197bb76aecbe9a0a7cf6feec75f7605b5e8008", size = 2857184, upload-time = "2025-12-09T13:37:49.96Z" }, - { url = "https://files.pythonhosted.org/packages/3a/f3/1b41d0b6a8b908aa07f652111155dd653ebbf0b3385e66562556c5206685/fonttools-4.60.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f86c8c37bc0ec0b9c141d5e90c717ff614e93c187f06d80f18c7057097f71bc", size = 2401877, upload-time = "2025-12-09T13:37:52.307Z" }, - { url = "https://files.pythonhosted.org/packages/71/57/048fd781680c38b05c5463657d0d95d5f2391a51972176e175c01de29d42/fonttools-4.60.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe905403fe59683b0e9a45f234af2866834376b8821f34633b1c76fb731b6311", size = 4878073, upload-time = "2025-12-09T13:37:56.477Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/363364f052a893cebd3d449588b21244a9d873620fda03ad92702d2e1bc7/fonttools-4.60.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38ce703b60a906e421e12d9e3a7f064883f5e61bb23e8961f4be33cfe578500b", size = 4835385, upload-time = "2025-12-09T13:37:58.882Z" }, - { url = "https://files.pythonhosted.org/packages/1c/38/e392bb930b2436287e6021672345db26441bf1f85f1e98f8b9784334e41d/fonttools-4.60.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9e810c06f3e79185cecf120e58b343ea5a89b54dd695fd644446bcf8c026da5e", size = 4853084, upload-time = "2025-12-09T13:38:01.578Z" }, - { url = "https://files.pythonhosted.org/packages/65/60/0d77faeaecf7a3276a8a6dc49e2274357e6b3ed6a1774e2fdb2a7f142db0/fonttools-4.60.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:38faec8cc1d12122599814d15a402183f5123fb7608dac956121e7c6742aebc5", size = 4971144, upload-time = "2025-12-09T13:38:03.748Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c7/6d3ac3afbcd598631bce24c3ecb919e7d0644a82fea8ddc4454312fc0be6/fonttools-4.60.2-cp39-cp39-win32.whl", hash = "sha256:80a45cf7bf659acb7b36578f300231873daba67bd3ca8cce181c73f861f14a37", size = 1499411, upload-time = "2025-12-09T13:38:05.586Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/9dedf6420e23f9fa630bb97941839dddd2e1e57d1b2b85a902378dbe0bd2/fonttools-4.60.2-cp39-cp39-win_amd64.whl", hash = "sha256:c355d5972071938e1b1e0f5a1df001f68ecf1a62f34a3407dc8e0beccf052501", size = 1547943, upload-time = "2025-12-09T13:38:07.604Z" }, - { url = "https://files.pythonhosted.org/packages/79/6c/10280af05b44fafd1dff69422805061fa1af29270bc52dce031ac69540bf/fonttools-4.60.2-py3-none-any.whl", hash = "sha256:73cf92eeda67cf6ff10c8af56fc8f4f07c1647d989a979be9e388a49be26552a", size = 1144610, upload-time = "2025-12-09T13:38:09.5Z" }, -] - [[package]] name = "fonttools" version = "4.61.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, @@ -1103,39 +739,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, ] -[[package]] -name = "fsspec" -version = "2025.10.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, -] - [[package]] name = "fsspec" version = "2026.1.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, @@ -1160,12 +767,10 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "iopath" }, { name = "numpy" }, - { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pillow" }, { name = "pyyaml" }, { name = "tabulate" }, - { name = "termcolor", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "termcolor", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "termcolor" }, { name = "tqdm" }, { name = "yacs" }, ] @@ -1234,16 +839,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d5/301e71c7d22a5c7aabf1953dd1106987bd47f883377d528355f898a850f2/grpcio-1.76.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:8ebe63ee5f8fa4296b1b8cfc743f870d10e902ca18afc65c68cf46fd39bb0783", size = 5840371, upload-time = "2025-10-21T16:22:42.468Z" }, - { url = "https://files.pythonhosted.org/packages/00/55/e3181adccff8808301dd9214b5e03c6db5a404b5ae8a6ec5768a5a65ed63/grpcio-1.76.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:3bf0f392c0b806905ed174dcd8bdd5e418a40d5567a05615a030a5aeddea692d", size = 11840384, upload-time = "2025-10-21T16:22:45.508Z" }, - { url = "https://files.pythonhosted.org/packages/65/36/db1dfe943bce7180f5b6d9be564366ca1024a005e914a1f10212c24a840b/grpcio-1.76.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b7604868b38c1bfd5cf72d768aedd7db41d78cb6a4a18585e33fb0f9f2363fd", size = 6408765, upload-time = "2025-10-21T16:22:48.761Z" }, - { url = "https://files.pythonhosted.org/packages/1e/79/a8452764aa4b5ca30a970e514ec2fc5cf75451571793f6b276b6807f67dc/grpcio-1.76.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e6d1db20594d9daba22f90da738b1a0441a7427552cc6e2e3d1297aeddc00378", size = 7076220, upload-time = "2025-10-21T16:22:51.546Z" }, - { url = "https://files.pythonhosted.org/packages/e0/61/4cca38c4e7bb3ac5a1e0be6cf700a4dd85c61cbd8a9c5e076c224967084e/grpcio-1.76.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d099566accf23d21037f18a2a63d323075bebace807742e4b0ac210971d4dd70", size = 6610195, upload-time = "2025-10-21T16:22:54.688Z" }, - { url = "https://files.pythonhosted.org/packages/54/3d/3f8bfae264c22c95fa702c35aa2a8105b754b4ace049c66a8b2230c97671/grpcio-1.76.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ebea5cc3aa8ea72e04df9913492f9a96d9348db876f9dda3ad729cfedf7ac416", size = 7193343, upload-time = "2025-10-21T16:22:57.434Z" }, - { url = "https://files.pythonhosted.org/packages/d1/cd/89f9254782b6cd94aa7c93fde370862877113b7189fb49900eaf9a706c82/grpcio-1.76.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0c37db8606c258e2ee0c56b78c62fc9dee0e901b5dbdcf816c2dd4ad652b8b0c", size = 8161922, upload-time = "2025-10-21T16:23:00.135Z" }, - { url = "https://files.pythonhosted.org/packages/af/e0/99eb899d7cb9c676afea70ab6d02a72a9e6ce24d0300f625773fafe6d547/grpcio-1.76.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ebebf83299b0cb1721a8859ea98f3a77811e35dce7609c5c963b9ad90728f886", size = 7617951, upload-time = "2025-10-21T16:23:03.68Z" }, - { url = "https://files.pythonhosted.org/packages/79/26/dca1b2bfaa9981cc28fa995730c80eedb0b86c912c30d1b676f08232e6ab/grpcio-1.76.0-cp39-cp39-win32.whl", hash = "sha256:0aaa82d0813fd4c8e589fac9b65d7dd88702555f702fb10417f96e2a2a6d4c0f", size = 3999306, upload-time = "2025-10-21T16:23:06.187Z" }, - { url = "https://files.pythonhosted.org/packages/de/d1/fb90564a981eedd3cd87dc6bfd7c249e8a515cfad1ed8e9af73be223cd3b/grpcio-1.76.0-cp39-cp39-win_amd64.whl", hash = "sha256:acab0277c40eff7143c2323190ea57b9ee5fd353d8190ee9652369fae735668a", size = 4708771, upload-time = "2025-10-21T16:23:08.902Z" }, ] [[package]] @@ -1303,10 +898,8 @@ name = "huggingface-hub" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "fsspec", version = "2026.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "filelock" }, + { name = "fsspec" }, { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, { name = "httpx" }, { name = "packaging" }, @@ -1350,71 +943,17 @@ version = "2.37.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, - { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pillow" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - -[[package]] -name = "importlib-resources" -version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, @@ -1438,77 +977,32 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ipywidgets" }, { name = "numpy" }, - { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pillow" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/56/484c8979bbcaa3e3f2da4eac6a1eb41e998e353e4c6ef89e9612889813c8/ipycanvas-0.14.3.tar.gz", hash = "sha256:c6a53a22eebf4d611b168b8f4434145883f27a7575509bd99a4bfc48c5385a39", size = 4150499, upload-time = "2025-12-11T09:12:59.916Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/11/37/c6880bd16093793dcb4c005011cf968f45fd815b7b5094fa8374524add26/ipycanvas-0.14.3-py2.py3-none-any.whl", hash = "sha256:8a2f48e1e079355d3e7d5683e5c6e7684a87c15c3750c8d8cd2289c95383ee3e", size = 142962, upload-time = "2025-12-11T09:12:50.5Z" }, ] -[[package]] -name = "ipykernel" -version = "6.31.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "appnope", marker = "python_full_version < '3.10' and sys_platform == 'darwin'" }, - { name = "comm", marker = "python_full_version < '3.10'" }, - { name = "debugpy", marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jupyter-client", version = "8.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, - { name = "nest-asyncio", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "psutil", marker = "python_full_version < '3.10'" }, - { name = "pyzmq", marker = "python_full_version < '3.10'" }, - { name = "tornado", marker = "python_full_version < '3.10'" }, - { name = "traitlets", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a5/1d/d5ba6edbfe6fae4c3105bca3a9c889563cc752c7f2de45e333164c7f4846/ipykernel-6.31.0.tar.gz", hash = "sha256:2372ce8bc1ff4f34e58cafed3a0feb2194b91fc7cad0fc72e79e47b45ee9e8f6", size = 167493, upload-time = "2025-10-20T11:42:39.948Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af", size = 117003, upload-time = "2025-10-20T11:42:37.502Z" }, -] - [[package]] name = "ipykernel" version = "7.1.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "appnope", marker = "python_full_version >= '3.10' and sys_platform == 'darwin'" }, - { name = "comm", marker = "python_full_version >= '3.10'" }, - { name = "debugpy", marker = "python_full_version >= '3.10'" }, - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jupyter-client", version = "8.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.10'" }, - { name = "nest-asyncio", marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "psutil", marker = "python_full_version >= '3.10'" }, - { name = "pyzmq", marker = "python_full_version >= '3.10'" }, - { name = "tornado", marker = "python_full_version >= '3.10'" }, - { name = "traitlets", marker = "python_full_version >= '3.10'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" } wheels = [ @@ -1520,15 +1014,12 @@ name = "ipympl" version = "0.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "ipywidgets" }, - { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib" }, { name = "numpy" }, - { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pillow" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/9c/f79e29f6262e821a15757662aa11cbb1db0a51ef836a32a46ddcb25e6832/ipympl-0.10.0.tar.gz", hash = "sha256:eda69602a010af2a42e8ebd069b0ee0dbe8df7fc69d7c1e8b99fece0a2fe613f", size = 3595672, upload-time = "2026-01-21T20:19:47.971Z" } @@ -1536,54 +1027,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/88c0ef22878c86035f058df0ac6c171319ffd0aa52a406455ed3a3847566/ipympl-0.10.0-py3-none-any.whl", hash = "sha256:a09c4f0ff86490cc62aed45e53b912fb706e3ec3506c4a51ce4a670d6667f5ce", size = 519020, upload-time = "2026-01-21T20:19:46.325Z" }, ] -[[package]] -name = "ipython" -version = "8.18.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.10'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "jedi", marker = "python_full_version < '3.10'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, - { name = "pexpect", marker = "python_full_version < '3.10' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "stack-data", marker = "python_full_version < '3.10'" }, - { name = "traitlets", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330, upload-time = "2023-11-27T09:58:34.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161, upload-time = "2023-11-27T09:58:30.538Z" }, -] - [[package]] name = "ipython" version = "8.38.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version == '3.10.*'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "jedi", marker = "python_full_version == '3.10.*'" }, - { name = "matplotlib-inline", marker = "python_full_version == '3.10.*'" }, - { name = "pexpect", marker = "python_full_version == '3.10.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version == '3.10.*'" }, - { name = "pygments", marker = "python_full_version == '3.10.*'" }, - { name = "stack-data", marker = "python_full_version == '3.10.*'" }, - { name = "traitlets", marker = "python_full_version == '3.10.*'" }, - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } wheels = [ @@ -1642,8 +1106,7 @@ version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "comm" }, - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jupyterlab-widgets" }, { name = "traitlets" }, @@ -1717,63 +1180,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, ] -[[package]] -name = "jsonschema" -version = "4.25.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "attrs", marker = "python_full_version < '3.10'" }, - { name = "jsonschema-specifications", marker = "python_full_version < '3.10'" }, - { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, -] - -[package.optional-dependencies] -format-nongpl = [ - { name = "fqdn", marker = "python_full_version < '3.10'" }, - { name = "idna", marker = "python_full_version < '3.10'" }, - { name = "isoduration", marker = "python_full_version < '3.10'" }, - { name = "jsonpointer", marker = "python_full_version < '3.10'" }, - { name = "rfc3339-validator", marker = "python_full_version < '3.10'" }, - { name = "rfc3986-validator", marker = "python_full_version < '3.10'" }, - { name = "rfc3987-syntax", marker = "python_full_version < '3.10'" }, - { name = "uri-template", marker = "python_full_version < '3.10'" }, - { name = "webcolors", version = "24.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] - [[package]] name = "jsonschema" version = "4.26.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "attrs", marker = "python_full_version >= '3.10'" }, - { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" }, - { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ @@ -1782,15 +1197,15 @@ wheels = [ [package.optional-dependencies] format-nongpl = [ - { name = "fqdn", marker = "python_full_version >= '3.10'" }, - { name = "idna", marker = "python_full_version >= '3.10'" }, - { name = "isoduration", marker = "python_full_version >= '3.10'" }, - { name = "jsonpointer", marker = "python_full_version >= '3.10'" }, - { name = "rfc3339-validator", marker = "python_full_version >= '3.10'" }, - { name = "rfc3986-validator", marker = "python_full_version >= '3.10'" }, - { name = "rfc3987-syntax", marker = "python_full_version >= '3.10'" }, - { name = "uri-template", marker = "python_full_version >= '3.10'" }, - { name = "webcolors", version = "25.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, ] [[package]] @@ -1798,8 +1213,7 @@ name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "referencing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ @@ -1811,8 +1225,7 @@ name = "jupyter" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipykernel", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ipykernel" }, { name = "ipywidgets" }, { name = "jupyter-console" }, { name = "jupyterlab" }, @@ -1821,56 +1234,19 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959, upload-time = "2024-08-30T07:15:48.299Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" }, -] - -[[package]] -name = "jupyter-client" -version = "8.6.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "python-dateutil", marker = "python_full_version < '3.10'" }, - { name = "pyzmq", marker = "python_full_version < '3.10'" }, - { name = "tornado", marker = "python_full_version < '3.10'" }, - { name = "traitlets", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, + { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" }, ] [[package]] name = "jupyter-client" version = "8.8.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, - { name = "pyzmq", marker = "python_full_version >= '3.10'" }, - { name = "tornado", marker = "python_full_version >= '3.10'" }, - { name = "traitlets", marker = "python_full_version >= '3.10'" }, + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } wheels = [ @@ -1882,15 +1258,11 @@ name = "jupyter-console" version = "6.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipykernel", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipykernel" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jupyter-client", version = "8.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jupyter-client", version = "8.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, { name = "prompt-toolkit" }, { name = "pygments" }, { name = "pyzmq" }, @@ -1901,47 +1273,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510, upload-time = "2023-03-06T14:13:28.229Z" }, ] -[[package]] -name = "jupyter-core" -version = "5.8.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pywin32", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, - { name = "traitlets", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" }, -] - [[package]] name = "jupyter-core" version = "5.9.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "traitlets", marker = "python_full_version >= '3.10'" }, + { name = "platformdirs" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } wheels = [ @@ -1953,13 +1291,11 @@ name = "jupyter-events" version = "0.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, extra = ["format-nongpl"], marker = "python_full_version < '3.10'" }, - { name = "jsonschema", version = "4.26.0", source = { registry = "https://pypi.org/simple" }, extra = ["format-nongpl"], marker = "python_full_version >= '3.10'" }, + { name = "jsonschema", extra = ["format-nongpl"] }, { name = "packaging" }, { name = "python-json-logger" }, { name = "pyyaml" }, - { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "referencing" }, { name = "rfc3339-validator" }, { name = "rfc3986-validator" }, { name = "traitlets" }, @@ -1974,7 +1310,6 @@ name = "jupyter-lsp" version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jupyter-server" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } @@ -1990,10 +1325,8 @@ dependencies = [ { name = "anyio" }, { name = "argon2-cffi" }, { name = "jinja2" }, - { name = "jupyter-client", version = "8.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jupyter-client", version = "8.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, { name = "jupyter-events" }, { name = "jupyter-server-terminals" }, { name = "nbconvert" }, @@ -2001,7 +1334,7 @@ dependencies = [ { name = "overrides", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "prometheus-client" }, - { name = "pywinpty", marker = "(python_full_version < '3.10' and os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "pyzmq" }, { name = "send2trash" }, { name = "terminado" }, @@ -2019,7 +1352,7 @@ name = "jupyter-server-terminals" version = "0.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywinpty", marker = "(python_full_version < '3.10' and os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "terminado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } @@ -2032,15 +1365,11 @@ name = "jupyterlab" version = "4.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "async-lru", version = "2.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "async-lru", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "async-lru" }, { name = "httpx" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipykernel", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ipykernel" }, { name = "jinja2" }, - { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-core" }, { name = "jupyter-lsp" }, { name = "jupyter-server" }, { name = "jupyterlab-server" }, @@ -2071,11 +1400,9 @@ version = "2.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "json5" }, - { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jsonschema", version = "4.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jsonschema" }, { name = "jupyter-server" }, { name = "packaging" }, { name = "requests" }, @@ -2094,114 +1421,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, ] -[[package]] -name = "kiwisolver" -version = "1.4.7" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440, upload-time = "2024-09-04T09:03:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758, upload-time = "2024-09-04T09:03:46.582Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311, upload-time = "2024-09-04T09:03:47.973Z" }, - { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109, upload-time = "2024-09-04T09:03:49.281Z" }, - { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814, upload-time = "2024-09-04T09:03:51.444Z" }, - { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881, upload-time = "2024-09-04T09:03:53.357Z" }, - { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972, upload-time = "2024-09-04T09:03:55.082Z" }, - { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787, upload-time = "2024-09-04T09:03:56.588Z" }, - { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212, upload-time = "2024-09-04T09:03:58.557Z" }, - { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399, upload-time = "2024-09-04T09:04:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688, upload-time = "2024-09-04T09:04:02.216Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493, upload-time = "2024-09-04T09:04:04.571Z" }, - { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191, upload-time = "2024-09-04T09:04:05.969Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644, upload-time = "2024-09-04T09:04:07.408Z" }, - { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877, upload-time = "2024-09-04T09:04:08.869Z" }, - { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347, upload-time = "2024-09-04T09:04:10.106Z" }, - { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442, upload-time = "2024-09-04T09:04:11.432Z" }, - { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762, upload-time = "2024-09-04T09:04:12.468Z" }, - { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319, upload-time = "2024-09-04T09:04:13.635Z" }, - { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260, upload-time = "2024-09-04T09:04:14.878Z" }, - { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589, upload-time = "2024-09-04T09:04:16.514Z" }, - { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080, upload-time = "2024-09-04T09:04:18.322Z" }, - { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049, upload-time = "2024-09-04T09:04:20.266Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376, upload-time = "2024-09-04T09:04:22.419Z" }, - { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231, upload-time = "2024-09-04T09:04:24.526Z" }, - { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634, upload-time = "2024-09-04T09:04:25.899Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024, upload-time = "2024-09-04T09:04:28.523Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484, upload-time = "2024-09-04T09:04:30.547Z" }, - { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078, upload-time = "2024-09-04T09:04:33.218Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645, upload-time = "2024-09-04T09:04:34.371Z" }, - { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022, upload-time = "2024-09-04T09:04:35.786Z" }, - { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536, upload-time = "2024-09-04T09:04:37.525Z" }, - { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808, upload-time = "2024-09-04T09:04:38.637Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531, upload-time = "2024-09-04T09:04:39.694Z" }, - { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894, upload-time = "2024-09-04T09:04:41.6Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296, upload-time = "2024-09-04T09:04:42.886Z" }, - { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450, upload-time = "2024-09-04T09:04:46.284Z" }, - { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168, upload-time = "2024-09-04T09:04:47.91Z" }, - { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308, upload-time = "2024-09-04T09:04:49.465Z" }, - { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186, upload-time = "2024-09-04T09:04:50.949Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877, upload-time = "2024-09-04T09:04:52.388Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204, upload-time = "2024-09-04T09:04:54.385Z" }, - { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461, upload-time = "2024-09-04T09:04:56.307Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358, upload-time = "2024-09-04T09:04:57.922Z" }, - { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119, upload-time = "2024-09-04T09:04:59.332Z" }, - { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367, upload-time = "2024-09-04T09:05:00.804Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884, upload-time = "2024-09-04T09:05:01.924Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528, upload-time = "2024-09-04T09:05:02.983Z" }, - { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449, upload-time = "2024-09-04T09:05:55.311Z" }, - { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757, upload-time = "2024-09-04T09:05:56.906Z" }, - { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312, upload-time = "2024-09-04T09:05:58.384Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966, upload-time = "2024-09-04T09:05:59.855Z" }, - { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044, upload-time = "2024-09-04T09:06:02.16Z" }, - { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879, upload-time = "2024-09-04T09:06:03.908Z" }, - { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751, upload-time = "2024-09-04T09:06:05.58Z" }, - { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990, upload-time = "2024-09-04T09:06:08.126Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122, upload-time = "2024-09-04T09:06:10.345Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126, upload-time = "2024-09-04T09:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313, upload-time = "2024-09-04T09:06:14.562Z" }, - { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784, upload-time = "2024-09-04T09:06:16.767Z" }, - { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988, upload-time = "2024-09-04T09:06:18.705Z" }, - { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980, upload-time = "2024-09-04T09:06:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847, upload-time = "2024-09-04T09:06:21.407Z" }, - { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494, upload-time = "2024-09-04T09:06:22.648Z" }, - { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491, upload-time = "2024-09-04T09:06:24.188Z" }, - { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648, upload-time = "2024-09-04T09:06:25.559Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257, upload-time = "2024-09-04T09:06:27.038Z" }, - { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906, upload-time = "2024-09-04T09:06:28.48Z" }, - { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951, upload-time = "2024-09-04T09:06:29.966Z" }, - { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715, upload-time = "2024-09-04T09:06:31.489Z" }, - { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666, upload-time = "2024-09-04T09:06:43.756Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088, upload-time = "2024-09-04T09:06:45.406Z" }, - { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321, upload-time = "2024-09-04T09:06:47.557Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776, upload-time = "2024-09-04T09:06:49.235Z" }, - { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984, upload-time = "2024-09-04T09:06:51.336Z" }, - { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811, upload-time = "2024-09-04T09:06:53.078Z" }, -] - [[package]] name = "kiwisolver" version = "1.4.9" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, @@ -2282,7 +1505,6 @@ version = "1.8.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/cd/337df968b38d94c5aabd3e1b10630f047a2b345f6e1d4456bd9fe7417537/libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b", size = 891354, upload-time = "2025-11-03T22:33:30.621Z" } wheels = [ @@ -2310,14 +1532,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/35/1d/317ddef3669883619ef3d3395ea583305f353ef4ad87d7a5ac1c39be38e3/libcst-1.8.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:375965f34cc6f09f5f809244d3ff9bd4f6cb6699f571121cebce53622e7e0b86", size = 2408239, upload-time = "2025-11-03T22:32:23.275Z" }, { url = "https://files.pythonhosted.org/packages/9a/a1/f47d8cccf74e212dd6044b9d6dbc223636508da99acff1d54786653196bc/libcst-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:da95b38693b989eaa8d32e452e8261cfa77fe5babfef1d8d2ac25af8c4aa7e6d", size = 2119660, upload-time = "2025-11-03T22:32:24.822Z" }, { url = "https://files.pythonhosted.org/packages/19/d0/dd313bf6a7942cdf951828f07ecc1a7695263f385065edc75ef3016a3cb5/libcst-1.8.6-cp312-cp312-win_arm64.whl", hash = "sha256:bff00e1c766658adbd09a175267f8b2f7616e5ee70ce45db3d7c4ce6d9f6bec7", size = 1999824, upload-time = "2025-11-03T22:32:26.131Z" }, - { url = "https://files.pythonhosted.org/packages/0c/09/69a0cd1eeb358f03c3ccd79ca22778afc1c1c723158270ad84ce86266eed/libcst-1.8.6-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cb2679ef532f9fa5be5c5a283b6357cb6e9888a8dd889c4bb2b01845a29d8c0b", size = 2211812, upload-time = "2025-11-03T22:33:17.748Z" }, - { url = "https://files.pythonhosted.org/packages/ff/38/b965fa7bc4409520404261ce6bdf019e56bed1674b9a68ddfc9e25bc904c/libcst-1.8.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:203ec2a83f259baf686b9526268cd23d048d38be5589594ef143aee50a4faf7e", size = 2093137, upload-time = "2025-11-03T22:33:19.457Z" }, - { url = "https://files.pythonhosted.org/packages/a9/7c/083084b91db049343c49a27279c226f4eb27d28bef4942965386418e643e/libcst-1.8.6-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6366ab2107425bf934b0c83311177f2a371bfc757ee8c6ad4a602d7cbcc2f363", size = 2237609, upload-time = "2025-11-03T22:33:21.083Z" }, - { url = "https://files.pythonhosted.org/packages/26/c5/fcf60600a809b9e4cf75e82484a7a9a4bdc80ba3c9939a6a18af3379c6c7/libcst-1.8.6-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:6aa11df6c58812f731172b593fcb485d7ba09ccc3b52fea6c7f26a43377dc748", size = 2301394, upload-time = "2025-11-03T22:33:22.847Z" }, - { url = "https://files.pythonhosted.org/packages/9f/73/d72942eb3f520bc9444e61a48236694dee3cdc13f6b59179e5288d725b93/libcst-1.8.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:351ab879c2fd20d9cb2844ed1ea3e617ed72854d3d1e2b0880ede9c3eea43ba8", size = 2301816, upload-time = "2025-11-03T22:33:24.295Z" }, - { url = "https://files.pythonhosted.org/packages/03/a9/5732b20569a434ee3ff96f1b263e6e3f3df70d8dba5cf7c8f7d4b1d6aa41/libcst-1.8.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98fa1ca321c81fb1f02e5c43f956ca543968cc1a30b264fd8e0a2e1b0b0bf106", size = 2408392, upload-time = "2025-11-03T22:33:25.873Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ad/ecb1275796504a34a9d6d5d4f73bd81cb12930064e98871ad4b4042b82e1/libcst-1.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:25fc7a1303cad7639ad45ec38c06789b4540b7258e9a108924aaa2c132af4aca", size = 2119206, upload-time = "2025-11-03T22:33:27.642Z" }, - { url = "https://files.pythonhosted.org/packages/94/32/b6521d32a7cde089380efa948e05a7cff95c7ece8f7c36380dd6b4bf2263/libcst-1.8.6-cp39-cp39-win_arm64.whl", hash = "sha256:4d7bbdd35f3abdfb5ac5d1a674923572dab892b126a58da81ff2726102d6ec2e", size = 2001882, upload-time = "2025-11-03T22:33:29.06Z" }, ] [[package]] @@ -2334,58 +1548,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/73/3d757cb3fc16f0f9794dd289bcd0c4a031d9cf54d8137d6b984b2d02edf3/lightning_utilities-0.15.2-py3-none-any.whl", hash = "sha256:ad3ab1703775044bbf880dbf7ddaaac899396c96315f3aa1779cec9d618a9841", size = 29431, upload-time = "2025-08-06T13:57:38.046Z" }, ] -[[package]] -name = "llvmlite" -version = "0.43.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/3d/f513755f285db51ab363a53e898b85562e950f79a2e6767a364530c2f645/llvmlite-0.43.0.tar.gz", hash = "sha256:ae2b5b5c3ef67354824fb75517c8db5fbe93bc02cd9671f3c62271626bc041d5", size = 157069, upload-time = "2024-06-13T18:09:32.641Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/ff/6ca7e98998b573b4bd6566f15c35e5c8bea829663a6df0c7aa55ab559da9/llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761", size = 31064408, upload-time = "2024-06-13T18:08:13.462Z" }, - { url = "https://files.pythonhosted.org/packages/ca/5c/a27f9257f86f0cda3f764ff21d9f4217b9f6a0d45e7a39ecfa7905f524ce/llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc", size = 28793153, upload-time = "2024-06-13T18:08:17.336Z" }, - { url = "https://files.pythonhosted.org/packages/7e/3c/4410f670ad0a911227ea2ecfcba9f672a77cf1924df5280c4562032ec32d/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d434ec7e2ce3cc8f452d1cd9a28591745de022f931d67be688a737320dfcead", size = 42857276, upload-time = "2024-06-13T18:08:21.071Z" }, - { url = "https://files.pythonhosted.org/packages/c6/21/2ffbab5714e72f2483207b4a1de79b2eecd9debbf666ff4e7067bcc5c134/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6912a87782acdff6eb8bf01675ed01d60ca1f2551f8176a300a886f09e836a6a", size = 43871781, upload-time = "2024-06-13T18:08:26.32Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/b5478037c453554a61625ef1125f7e12bb1429ae11c6376f47beba9b0179/llvmlite-0.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:14f0e4bf2fd2d9a75a3534111e8ebeb08eda2f33e9bdd6dfa13282afacdde0ed", size = 28123487, upload-time = "2024-06-13T18:08:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/95/8c/de3276d773ab6ce3ad676df5fab5aac19696b2956319d65d7dd88fb10f19/llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8d0618cb9bfe40ac38a9633f2493d4d4e9fcc2f438d39a4e854f39cc0f5f98", size = 31064409, upload-time = "2024-06-13T18:08:34.006Z" }, - { url = "https://files.pythonhosted.org/packages/ee/e1/38deed89ced4cf378c61e232265cfe933ccde56ae83c901aa68b477d14b1/llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0a9a1a39d4bf3517f2af9d23d479b4175ead205c592ceeb8b89af48a327ea57", size = 28793149, upload-time = "2024-06-13T18:08:37.42Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b2/4429433eb2dc8379e2cb582502dca074c23837f8fd009907f78a24de4c25/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1da416ab53e4f7f3bc8d4eeba36d801cc1894b9fbfbf2022b29b6bad34a7df2", size = 42857277, upload-time = "2024-06-13T18:08:40.822Z" }, - { url = "https://files.pythonhosted.org/packages/6b/99/5d00a7d671b1ba1751fc9f19d3b36f3300774c6eebe2bcdb5f6191763eb4/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977525a1e5f4059316b183fb4fd34fa858c9eade31f165427a3977c95e3ee749", size = 43871781, upload-time = "2024-06-13T18:08:46.41Z" }, - { url = "https://files.pythonhosted.org/packages/20/ab/ed5ed3688c6ba4f0b8d789da19fd8e30a9cf7fc5852effe311bc5aefe73e/llvmlite-0.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5bd550001d26450bd90777736c69d68c487d17bf371438f975229b2b8241a91", size = 28107433, upload-time = "2024-06-13T18:08:50.834Z" }, - { url = "https://files.pythonhosted.org/packages/0b/67/9443509e5d2b6d8587bae3ede5598fa8bd586b1c7701696663ea8af15b5b/llvmlite-0.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f99b600aa7f65235a5a05d0b9a9f31150c390f31261f2a0ba678e26823ec38f7", size = 31064409, upload-time = "2024-06-13T18:08:54.375Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9c/24139d3712d2d352e300c39c0e00d167472c08b3bd350c3c33d72c88ff8d/llvmlite-0.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:35d80d61d0cda2d767f72de99450766250560399edc309da16937b93d3b676e7", size = 28793145, upload-time = "2024-06-13T18:08:57.953Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f1/4c205a48488e574ee9f6505d50e84370a978c90f08dab41a42d8f2c576b6/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eccce86bba940bae0d8d48ed925f21dbb813519169246e2ab292b5092aba121f", size = 42857276, upload-time = "2024-06-13T18:09:02.067Z" }, - { url = "https://files.pythonhosted.org/packages/00/5f/323c4d56e8401c50185fd0e875fcf06b71bf825a863699be1eb10aa2a9cb/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6509e1507ca0760787a199d19439cc887bfd82226f5af746d6977bd9f66844", size = 43871781, upload-time = "2024-06-13T18:09:06.667Z" }, - { url = "https://files.pythonhosted.org/packages/c6/94/dea10e263655ce78d777e78d904903faae39d1fc440762be4a9dc46bed49/llvmlite-0.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a2872ee80dcf6b5dbdc838763d26554c2a18aa833d31a2635bff16aafefb9c9", size = 28107442, upload-time = "2024-06-13T18:09:10.709Z" }, - { url = "https://files.pythonhosted.org/packages/2a/73/12925b1bbb3c2beb6d96f892ef5b4d742c34f00ddb9f4a125e9e87b22f52/llvmlite-0.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cd2a7376f7b3367019b664c21f0c61766219faa3b03731113ead75107f3b66c", size = 31064410, upload-time = "2024-06-13T18:09:14.091Z" }, - { url = "https://files.pythonhosted.org/packages/cc/61/58c70aa0808a8cba825a7d98cc65bef4801b99328fba80837bfcb5fc767f/llvmlite-0.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18e9953c748b105668487b7c81a3e97b046d8abf95c4ddc0cd3c94f4e4651ae8", size = 28793145, upload-time = "2024-06-13T18:09:17.531Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c6/9324eb5de2ba9d99cbed853d85ba7a318652a48e077797bec27cf40f911d/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74937acd22dc11b33946b67dca7680e6d103d6e90eeaaaf932603bec6fe7b03a", size = 42857276, upload-time = "2024-06-13T18:09:21.377Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d0/889e9705107db7b1ec0767b03f15d7b95b4c4f9fdf91928ab1c7e9ffacf6/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9efc739cc6ed760f795806f67889923f7274276f0eb45092a1473e40d9b867", size = 43871777, upload-time = "2024-06-13T18:09:25.76Z" }, - { url = "https://files.pythonhosted.org/packages/df/41/73cc26a2634b538cfe813f618c91e7e9960b8c163f8f0c94a2b0f008b9da/llvmlite-0.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:47e147cdda9037f94b399bf03bfd8a6b6b1f2f90be94a454e3386f006455a9b4", size = 28123489, upload-time = "2024-06-13T18:09:29.78Z" }, -] - [[package]] name = "llvmlite" version = "0.46.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3d/a4/3959e1c61c5ca9db7921e5fd115b344c29b9d57a5dadd87bef97963ca1a5/llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0", size = 37232766, upload-time = "2025-12-08T18:14:34.765Z" }, @@ -2402,42 +1568,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" }, ] -[[package]] -name = "markdown" -version = "3.9" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, -] - [[package]] name = "markdown" version = "3.10.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" }, @@ -2482,102 +1616,23 @@ 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/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, - { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, - { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, - { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, - { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, -] - -[[package]] -name = "matplotlib" -version = "3.9.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "cycler", marker = "python_full_version < '3.10'" }, - { name = "fonttools", version = "4.60.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "importlib-resources", marker = "python_full_version < '3.10'" }, - { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pyparsing", marker = "python_full_version < '3.10'" }, - { name = "python-dateutil", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529, upload-time = "2024-12-13T05:56:34.184Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089, upload-time = "2024-12-13T05:54:24.224Z" }, - { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600, upload-time = "2024-12-13T05:54:27.214Z" }, - { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138, upload-time = "2024-12-13T05:54:29.497Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711, upload-time = "2024-12-13T05:54:34.396Z" }, - { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622, upload-time = "2024-12-13T05:54:36.808Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211, upload-time = "2024-12-13T05:54:40.596Z" }, - { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430, upload-time = "2024-12-13T05:54:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045, upload-time = "2024-12-13T05:54:46.414Z" }, - { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906, upload-time = "2024-12-13T05:54:49.459Z" }, - { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873, upload-time = "2024-12-13T05:54:53.066Z" }, - { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566, upload-time = "2024-12-13T05:54:55.522Z" }, - { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065, upload-time = "2024-12-13T05:54:58.337Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131, upload-time = "2024-12-13T05:55:02.837Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365, upload-time = "2024-12-13T05:55:05.158Z" }, - { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707, upload-time = "2024-12-13T05:55:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761, upload-time = "2024-12-13T05:55:12.95Z" }, - { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284, upload-time = "2024-12-13T05:55:16.199Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160, upload-time = "2024-12-13T05:55:19.991Z" }, - { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919, upload-time = "2024-12-13T05:55:59.66Z" }, - { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486, upload-time = "2024-12-13T05:56:04.264Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838, upload-time = "2024-12-13T05:56:06.792Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492, upload-time = "2024-12-13T05:56:09.964Z" }, - { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500, upload-time = "2024-12-13T05:56:13.55Z" }, - { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962, upload-time = "2024-12-13T05:56:16.358Z" }, - { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995, upload-time = "2024-12-13T05:56:18.805Z" }, - { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300, upload-time = "2024-12-13T05:56:21.315Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423, upload-time = "2024-12-13T05:56:26.719Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624, upload-time = "2024-12-13T05:56:29.359Z" }, ] [[package]] name = "matplotlib" version = "3.10.8" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "cycler", marker = "python_full_version >= '3.10'" }, - { name = "fonttools", version = "4.61.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "kiwisolver", version = "1.4.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pyparsing", marker = "python_full_version >= '3.10'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } wheels = [ @@ -2638,8 +1693,7 @@ name = "moreorless" version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8d/85/2e4999ac4a21ab3c5f31e2a48e0989a80be3afc512a7983e3253615983d4/moreorless-0.5.0.tar.gz", hash = "sha256:560a04f85006fccd74feaa4b6213a446392ff7b5ec0194a5464b6c30f182fa33", size = 14093, upload-time = "2025-05-04T22:29:59.006Z" } wheels = [ @@ -2664,50 +1718,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -[[package]] -name = "nbclient" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "jupyter-client", version = "8.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "nbformat", marker = "python_full_version < '3.10'" }, - { name = "traitlets", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424, upload-time = "2024-12-19T10:32:27.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434, upload-time = "2024-12-19T10:32:24.139Z" }, -] - [[package]] name = "nbclient" version = "0.10.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "jupyter-client", version = "8.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "nbformat", marker = "python_full_version >= '3.10'" }, - { name = "traitlets", marker = "python_full_version >= '3.10'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } wheels = [ @@ -2720,18 +1739,14 @@ version = "7.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, - { name = "bleach", version = "6.2.0", source = { registry = "https://pypi.org/simple" }, extra = ["css"], marker = "python_full_version < '3.10'" }, - { name = "bleach", version = "6.3.0", source = { registry = "https://pypi.org/simple" }, extra = ["css"], marker = "python_full_version >= '3.10'" }, + { name = "bleach", extra = ["css"] }, { name = "defusedxml" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, - { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jupyter-core" }, { name = "jupyterlab-pygments" }, { name = "markupsafe" }, { name = "mistune" }, - { name = "nbclient", version = "0.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "nbclient", version = "0.10.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "nbclient" }, { name = "nbformat" }, { name = "packaging" }, { name = "pandocfilters" }, @@ -2749,10 +1764,8 @@ version = "5.10.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastjsonschema" }, - { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jsonschema", version = "4.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } @@ -2769,28 +1782,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] -[[package]] -name = "networkx" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/80/a84676339aaae2f1cfdf9f418701dd634aef9cc76f708ef55c36ff39c3ca/networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6", size = 2073928, upload-time = "2023-10-28T08:41:39.364Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/f0/8fbc882ca80cf077f1b246c0e3c3465f7f415439bdea6b899f6b19f61f70/networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2", size = 1647772, upload-time = "2023-10-28T08:41:36.945Z" }, -] - [[package]] name = "networkx" version = "3.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' 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 = [ @@ -2846,65 +1845,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, ] -[[package]] -name = "numba" -version = "0.60.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "llvmlite", version = "0.43.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3c/93/2849300a9184775ba274aba6f82f303343669b0592b7bb0849ea713dabb0/numba-0.60.0.tar.gz", hash = "sha256:5df6158e5584eece5fc83294b949fd30b9f1125df7708862205217e068aabf16", size = 2702171, upload-time = "2024-06-13T18:11:19.869Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/cf/baa13a7e3556d73d9e38021e6d6aa4aeb30d8b94545aa8b70d0f24a1ccc4/numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651", size = 2647627, upload-time = "2024-06-13T18:10:29.857Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ba/4b57fa498564457c3cc9fc9e570a6b08e6086c74220f24baaf04e54b995f/numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b", size = 2650322, upload-time = "2024-06-13T18:10:32.849Z" }, - { url = "https://files.pythonhosted.org/packages/28/98/7ea97ee75870a54f938a8c70f7e0be4495ba5349c5f9db09d467c4a5d5b7/numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1527dc578b95c7c4ff248792ec33d097ba6bef9eda466c948b68dfc995c25781", size = 3407390, upload-time = "2024-06-13T18:10:34.741Z" }, - { url = "https://files.pythonhosted.org/packages/79/58/cb4ac5b8f7ec64200460aef1fed88258fb872ceef504ab1f989d2ff0f684/numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe0b28abb8d70f8160798f4de9d486143200f34458d34c4a214114e445d7124e", size = 3699694, upload-time = "2024-06-13T18:10:37.295Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b0/c61a93ca947d12233ff45de506ddbf52af3f752066a0b8be4d27426e16da/numba-0.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:19407ced081d7e2e4b8d8c36aa57b7452e0283871c296e12d798852bc7d7f198", size = 2687030, upload-time = "2024-06-13T18:10:39.47Z" }, - { url = "https://files.pythonhosted.org/packages/98/ad/df18d492a8f00d29a30db307904b9b296e37507034eedb523876f3a2e13e/numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a17b70fc9e380ee29c42717e8cc0bfaa5556c416d94f9aa96ba13acb41bdece8", size = 2647254, upload-time = "2024-06-13T18:10:41.69Z" }, - { url = "https://files.pythonhosted.org/packages/9a/51/a4dc2c01ce7a850b8e56ff6d5381d047a5daea83d12bad08aa071d34b2ee/numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fb02b344a2a80efa6f677aa5c40cd5dd452e1b35f8d1c2af0dfd9ada9978e4b", size = 2649970, upload-time = "2024-06-13T18:10:44.682Z" }, - { url = "https://files.pythonhosted.org/packages/f9/4c/8889ac94c0b33dca80bed11564b8c6d9ea14d7f094e674c58e5c5b05859b/numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f4fde652ea604ea3c86508a3fb31556a6157b2c76c8b51b1d45eb40c8598703", size = 3412492, upload-time = "2024-06-13T18:10:47.1Z" }, - { url = "https://files.pythonhosted.org/packages/57/03/2b4245b05b71c0cee667e6a0b51606dfa7f4157c9093d71c6b208385a611/numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4142d7ac0210cc86432b818338a2bc368dc773a2f5cf1e32ff7c5b378bd63ee8", size = 3705018, upload-time = "2024-06-13T18:10:49.539Z" }, - { url = "https://files.pythonhosted.org/packages/79/89/2d924ca60dbf949f18a6fec223a2445f5f428d9a5f97a6b29c2122319015/numba-0.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cac02c041e9b5bc8cf8f2034ff6f0dbafccd1ae9590dc146b3a02a45e53af4e2", size = 2686920, upload-time = "2024-06-13T18:10:51.937Z" }, - { url = "https://files.pythonhosted.org/packages/eb/5c/b5ec752c475e78a6c3676b67c514220dbde2725896bbb0b6ec6ea54b2738/numba-0.60.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7da4098db31182fc5ffe4bc42c6f24cd7d1cb8a14b59fd755bfee32e34b8404", size = 2647866, upload-time = "2024-06-13T18:10:54.453Z" }, - { url = "https://files.pythonhosted.org/packages/65/42/39559664b2e7c15689a638c2a38b3b74c6e69a04e2b3019b9f7742479188/numba-0.60.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38d6ea4c1f56417076ecf8fc327c831ae793282e0ff51080c5094cb726507b1c", size = 2650208, upload-time = "2024-06-13T18:10:56.779Z" }, - { url = "https://files.pythonhosted.org/packages/67/88/c4459ccc05674ef02119abf2888ccd3e2fed12a323f52255f4982fc95876/numba-0.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62908d29fb6a3229c242e981ca27e32a6e606cc253fc9e8faeb0e48760de241e", size = 3466946, upload-time = "2024-06-13T18:10:58.961Z" }, - { url = "https://files.pythonhosted.org/packages/8b/41/ac11cf33524def12aa5bd698226ae196a1185831c05ed29dc0c56eaa308b/numba-0.60.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ebaa91538e996f708f1ab30ef4d3ddc344b64b5227b67a57aa74f401bb68b9d", size = 3761463, upload-time = "2024-06-13T18:11:01.657Z" }, - { url = "https://files.pythonhosted.org/packages/ca/bd/0fe29fcd1b6a8de479a4ed25c6e56470e467e3611c079d55869ceef2b6d1/numba-0.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:f75262e8fe7fa96db1dca93d53a194a38c46da28b112b8a4aca168f0df860347", size = 2707588, upload-time = "2024-06-13T18:11:04.261Z" }, - { url = "https://files.pythonhosted.org/packages/68/1a/87c53f836cdf557083248c3f47212271f220280ff766538795e77c8c6bbf/numba-0.60.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:01ef4cd7d83abe087d644eaa3d95831b777aa21d441a23703d649e06b8e06b74", size = 2647186, upload-time = "2024-06-13T18:11:06.753Z" }, - { url = "https://files.pythonhosted.org/packages/28/14/a5baa1f2edea7b49afa4dc1bb1b126645198cf1075186853b5b497be826e/numba-0.60.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:819a3dfd4630d95fd574036f99e47212a1af41cbcb019bf8afac63ff56834449", size = 2650038, upload-time = "2024-06-13T18:11:10.869Z" }, - { url = "https://files.pythonhosted.org/packages/3b/bd/f1985719ff34e37e07bb18f9d3acd17e5a21da255f550c8eae031e2ddf5f/numba-0.60.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b983bd6ad82fe868493012487f34eae8bf7dd94654951404114f23c3466d34b", size = 3403010, upload-time = "2024-06-13T18:11:13.057Z" }, - { url = "https://files.pythonhosted.org/packages/54/9b/cd73d3f6617ddc8398a63ef97d8dc9139a9879b9ca8a7ca4b8789056ea46/numba-0.60.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c151748cd269ddeab66334bd754817ffc0cabd9433acb0f551697e5151917d25", size = 3695086, upload-time = "2024-06-13T18:11:15.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/01/8b7b670c77c5ea0e47e283d82332969bf672ab6410d0b2610cac5b7a3ded/numba-0.60.0-cp39-cp39-win_amd64.whl", hash = "sha256:3031547a015710140e8c87226b4cfe927cac199835e5bf7d4fe5cb64e814e3ab", size = 2686978, upload-time = "2024-06-13T18:11:17.765Z" }, -] - [[package]] name = "numba" version = "0.63.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "llvmlite", version = "0.46.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", marker = "python_full_version >= '3.10'" }, + { name = "llvmlite" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" } wheels = [ @@ -2952,167 +1899,155 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, - { url = "https://files.pythonhosted.org/packages/7d/24/ce71dc08f06534269f66e73c04f5709ee024a1afe92a7b6e1d73f158e1f8/numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c", size = 20636301, upload-time = "2024-02-05T23:59:10.976Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8c/ab03a7c25741f9ebc92684a20125fbc9fc1b8e1e700beb9197d750fdff88/numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be", size = 13971216, upload-time = "2024-02-05T23:59:35.472Z" }, - { url = "https://files.pythonhosted.org/packages/6d/64/c3bcdf822269421d85fe0d64ba972003f9bb4aa9a419da64b86856c9961f/numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764", size = 14226281, upload-time = "2024-02-05T23:59:59.372Z" }, - { url = "https://files.pythonhosted.org/packages/54/30/c2a907b9443cf42b90c17ad10c1e8fa801975f01cb9764f3f8eb8aea638b/numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", size = 18249516, upload-time = "2024-02-06T00:00:32.79Z" }, - { url = "https://files.pythonhosted.org/packages/43/12/01a563fc44c07095996d0129b8899daf89e4742146f7044cdbdb3101c57f/numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd", size = 13882132, upload-time = "2024-02-06T00:00:58.197Z" }, - { url = "https://files.pythonhosted.org/packages/16/ee/9df80b06680aaa23fc6c31211387e0db349e0e36d6a63ba3bd78c5acdf11/numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c", size = 18084181, upload-time = "2024-02-06T00:01:31.21Z" }, - { url = "https://files.pythonhosted.org/packages/28/7d/4b92e2fe20b214ffca36107f1a3e75ef4c488430e64de2d9af5db3a4637d/numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6", size = 5976360, upload-time = "2024-02-06T00:01:43.013Z" }, - { url = "https://files.pythonhosted.org/packages/b5/42/054082bd8220bbf6f297f982f0a8f5479fcbc55c8b511d928df07b965869/numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea", size = 15814633, upload-time = "2024-02-06T00:02:16.694Z" }, - { url = "https://files.pythonhosted.org/packages/3f/72/3df6c1c06fc83d9cfe381cccb4be2532bbd38bf93fbc9fad087b6687f1c0/numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30", size = 20455961, upload-time = "2024-02-06T00:03:05.993Z" }, - { url = "https://files.pythonhosted.org/packages/8e/02/570545bac308b58ffb21adda0f4e220ba716fb658a63c151daecc3293350/numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c", size = 18061071, upload-time = "2024-02-06T00:03:41.5Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5f/fafd8c51235f60d49f7a88e2275e13971e90555b67da52dd6416caec32fe/numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0", size = 15709730, upload-time = "2024-02-06T00:04:11.719Z" }, ] [[package]] -name = "nvidia-cublas-cu12" -version = "12.8.4.1" +name = "nvidia-cublas" +version = "13.1.0.3" 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/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, ] [[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.8.90" +name = "nvidia-cuda-cupti" +version = "13.0.85" 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/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, ] [[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.8.93" +name = "nvidia-cuda-nvrtc" +version = "13.0.88" 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/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, ] [[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.8.90" +name = "nvidia-cuda-runtime" +version = "13.0.96" 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/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, ] [[package]] -name = "nvidia-cudnn-cu12" -version = "9.10.2.21" +name = "nvidia-cudnn-cu13" +version = "9.19.0.56" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "nvidia-cublas" }, ] 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/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, ] [[package]] -name = "nvidia-cufft-cu12" -version = "11.3.3.83" +name = "nvidia-cufft" +version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "nvidia-nvjitlink" }, ] 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" }, + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, ] [[package]] -name = "nvidia-cufile-cu12" -version = "1.13.1.3" +name = "nvidia-cufile" +version = "1.15.1.6" 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/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, ] [[package]] -name = "nvidia-curand-cu12" -version = "10.3.9.90" +name = "nvidia-curand" +version = "10.4.0.35" 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/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, ] [[package]] -name = "nvidia-cusolver-cu12" -version = "11.7.3.90" +name = "nvidia-cusolver" +version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "nvidia-cusparse-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, ] 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/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, ] [[package]] -name = "nvidia-cusparse-cu12" -version = "12.5.8.93" +name = "nvidia-cusparse" +version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, -] -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" }, + { name = "nvidia-nvjitlink" }, ] - -[[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/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, ] [[package]] -name = "nvidia-nccl-cu12" -version = "2.27.3" +name = "nvidia-cusparselt-cu13" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/5b/4e4fff7bad39adf89f735f2bc87248c81db71205b62bcc0d5ca5b606b3c3/nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adf27ccf4238253e0b826bce3ff5fa532d65fc42322c8bfdfaf28024c0fbe039", size = 322364134, upload-time = "2025-06-03T21:58:04.013Z" }, + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, ] [[package]] -name = "nvidia-nccl-cu12" -version = "2.27.5" +name = "nvidia-nccl-cu13" +version = "2.28.9" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] 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/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, ] [[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.8.93" +name = "nvidia-nvjitlink" +version = "13.0.88" source = { registry = "https://pypi.org/simple" } 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" }, + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, ] [[package]] -name = "nvidia-nvshmem-cu12" +name = "nvidia-nvshmem-cu13" 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" }, + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, ] [[package]] -name = "nvidia-nvtx-cu12" -version = "12.8.90" +name = "nvidia-nvtx" +version = "13.0.85" 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/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, ] [[package]] @@ -3168,12 +2103,9 @@ name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ { name = "numpy", marker = "python_full_version < '3.11'" }, @@ -3204,13 +2136,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, - { url = "https://files.pythonhosted.org/packages/56/b4/52eeb530a99e2a4c55ffcd352772b599ed4473a0f892d127f4147cf0f88e/pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2", size = 11567720, upload-time = "2025-09-29T23:33:06.209Z" }, - { url = "https://files.pythonhosted.org/packages/48/4a/2d8b67632a021bced649ba940455ed441ca854e57d6e7658a6024587b083/pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8", size = 10810302, upload-time = "2025-09-29T23:33:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/13/e6/d2465010ee0569a245c975dc6967b801887068bc893e908239b1f4b6c1ac/pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff", size = 12154874, upload-time = "2025-09-29T23:33:49.939Z" }, - { url = "https://files.pythonhosted.org/packages/1f/18/aae8c0aa69a386a3255940e9317f793808ea79d0a525a97a903366bb2569/pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29", size = 12790141, upload-time = "2025-09-29T23:34:05.655Z" }, - { url = "https://files.pythonhosted.org/packages/f7/26/617f98de789de00c2a444fbe6301bb19e66556ac78cff933d2c98f62f2b4/pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73", size = 13208697, upload-time = "2025-09-29T23:34:21.835Z" }, - { url = "https://files.pythonhosted.org/packages/b9/fb/25709afa4552042bd0e15717c75e9b4a2294c3dc4f7e6ea50f03c5136600/pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9", size = 13879233, upload-time = "2025-09-29T23:34:35.079Z" }, - { url = "https://files.pythonhosted.org/packages/98/af/7be05277859a7bc399da8ba68b88c96b27b48740b6cf49688899c6eb4176/pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa", size = 11359119, upload-time = "2025-09-29T23:34:46.339Z" }, ] [[package]] @@ -3293,96 +2218,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] -[[package]] -name = "pillow" -version = "11.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, - { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, - { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, - { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, - { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, - { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, - { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, - { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, - { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, - { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, - { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, - { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, - { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, - { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, - { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, - { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, - { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478, upload-time = "2025-07-01T09:15:52.209Z" }, - { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522, upload-time = "2025-07-01T09:15:54.162Z" }, - { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376, upload-time = "2025-07-03T13:11:01.066Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020, upload-time = "2025-07-03T13:11:06.479Z" }, - { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732, upload-time = "2025-07-01T09:15:56.111Z" }, - { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404, upload-time = "2025-07-01T09:15:58.245Z" }, - { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760, upload-time = "2025-07-01T09:16:00.003Z" }, - { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534, upload-time = "2025-07-01T09:16:02.29Z" }, - { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091, upload-time = "2025-07-01T09:16:04.4Z" }, - { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091, upload-time = "2025-07-01T09:16:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632, upload-time = "2025-07-01T09:16:08.142Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, - { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, - { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, - { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, - { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, - { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, - { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, -] - [[package]] name = "pillow" version = "12.1.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, @@ -3427,39 +2266,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, ] -[[package]] -name = "platformdirs" -version = "4.4.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, -] - [[package]] name = "platformdirs" version = "4.5.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, @@ -3519,8 +2329,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/08/60/84d5f6dcda9165e4d6a56ac8433c9f40a8906bf2966150b8a0cfde097d78/protobuf-6.33.5-cp39-cp39-win32.whl", hash = "sha256:a3157e62729aafb8df6da2c03aa5c0937c7266c626ce11a278b6eb7963c4e37c", size = 425892, upload-time = "2026-01-29T21:51:30.382Z" }, - { url = "https://files.pythonhosted.org/packages/68/19/33d7dc2dc84439587fa1e21e1c0026c01ad2af0a62f58fd54002a7546307/protobuf-6.33.5-cp39-cp39-win_amd64.whl", hash = "sha256:8f04fa32763dcdb4973d537d6b54e615cc61108c7cb38fe59310c3192d29510a", size = 437137, upload-time = "2026-01-29T21:51:31.456Z" }, { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] @@ -3582,54 +2390,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/23/d17f6111c2a6ae8631d4fa90202bea05844da715d61431fbc34d276462d5/pycocotools-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:8e159232adae3aef6b4e2d37b008bff107b26e9ed3b48e70ea6482302834bd34", size = 80519, upload-time = "2025-12-15T22:31:09.613Z" }, { url = "https://files.pythonhosted.org/packages/00/4c/76b00b31a724c3f5ccdab0f85e578afb2ca38d33be0a0e98f1770cafd958/pycocotools-2.0.11-cp311-cp311-win_arm64.whl", hash = "sha256:4fc9889e819452b9c142036e1eabac8a13a8bd552d8beba299a57e0da6bfa1ec", size = 69304, upload-time = "2025-12-15T22:31:10.592Z" }, { url = "https://files.pythonhosted.org/packages/87/12/2f2292332456e4e4aba1dec0e3de8f1fc40fb2f4fdb0ca1cb17db9861682/pycocotools-2.0.11-cp312-abi3-macosx_10_13_universal2.whl", hash = "sha256:a2e9634bc7cadfb01c88e0b98589aaf0bd12983c7927bde93f19c0103e5441f4", size = 147795, upload-time = "2025-12-15T22:31:11.519Z" }, - { url = "https://files.pythonhosted.org/packages/63/3c/68d7ea376aada9046e7ea2d7d0dad0d27e1ae8b4b3c26a28346689390ab2/pycocotools-2.0.11-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fd4121766cc057133534679c0ec3f9023dbd96e9b31cf95c86a069ebdac2b65", size = 398434, upload-time = "2025-12-15T22:31:12.558Z" }, - { url = "https://files.pythonhosted.org/packages/23/59/dc81895beff4e1207a829d40d442ea87cefaac9f6499151965f05c479619/pycocotools-2.0.11-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a82d1c9ed83f75da0b3f244f2a3cf559351a283307bd9b79a4ee2b93ab3231dd", size = 411685, upload-time = "2025-12-15T22:31:13.995Z" }, - { url = "https://files.pythonhosted.org/packages/0b/0b/5a8a7de300862a2eb5e2ecd3cb015126231379206cd3ebba8f025388d770/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89e853425018e2c2920ee0f2112cf7c140a1dcf5f4f49abd9c2da112c3e0f4b3", size = 390500, upload-time = "2025-12-15T22:31:15.138Z" }, - { url = "https://files.pythonhosted.org/packages/63/b5/519bb68647f06feea03d5f355c33c05800aeae4e57b9482b2859eb00752e/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87af87b8d06d5b852a885a319d9362dca3bed9f8bbcc3feb6513acb1f88ea242", size = 409790, upload-time = "2025-12-15T22:31:16.326Z" }, - { url = "https://files.pythonhosted.org/packages/83/b4/f6708404ff494706b80e714b919f76dc4ec9845a4007affd6d6b0843f928/pycocotools-2.0.11-cp312-abi3-win_amd64.whl", hash = "sha256:ffe806ce535f5996445188f9a35643791dc54beabc61bd81e2b03367356d604f", size = 77570, upload-time = "2025-12-15T22:31:17.703Z" }, - { url = "https://files.pythonhosted.org/packages/6e/63/778cd0ddc9d4a78915ac0a72b56d7fb204f7c3fabdad067d67ea0089762e/pycocotools-2.0.11-cp312-abi3-win_arm64.whl", hash = "sha256:c230f5e7b14bd19085217b4f40bba81bf14a182b150b8e9fab1c15d504ade343", size = 64564, upload-time = "2025-12-15T22:31:18.652Z" }, - { url = "https://files.pythonhosted.org/packages/fa/8a/e005c50aef072bb3800b8f38d77dd6d9a08f91efb17b65cffa7e8eaa3abc/pycocotools-2.0.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9dc8b388984c72aa84b1a68933f196ce71ab114c59232d0eab20c97cc1300875", size = 161170, upload-time = "2025-12-15T22:31:38.08Z" }, - { url = "https://files.pythonhosted.org/packages/04/59/d14d3adcf0dbe63774d80717bf865fc3b77822ed41ab7ae308c48192024d/pycocotools-2.0.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2b018497ec198ffc737dd7e6306a2e69999779ca619a9e12950e4506e410c3e", size = 461544, upload-time = "2025-12-15T22:31:39.151Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3c/a617217c3a5d8313ea656ded7f38ae34f78ad95be44bba12890faf389c6b/pycocotools-2.0.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e72527fdc00985d29dbe31d17b19cd2d16fbad7b01e974c567b593d5844de710", size = 470263, upload-time = "2025-12-15T22:31:40.429Z" }, - { url = "https://files.pythonhosted.org/packages/60/04/cc6dd61b27c58bed41fc0efc08e1405147c9146542b5955db5915a52a0e9/pycocotools-2.0.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d6b88557166794a24acd03f50296f2b95adf3e4206b4b8995e6bdca925c9cb72", size = 456222, upload-time = "2025-12-15T22:31:41.576Z" }, - { url = "https://files.pythonhosted.org/packages/21/6f/7db99a9faae1d1fa8ad9b9ca528b1144e76c849eb5c037bb0f8bda665320/pycocotools-2.0.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6a2f4f036f5bfdaf8c9625279051e921721fc9d27f92c97a3f3355a07ba38513", size = 475143, upload-time = "2025-12-15T22:31:42.822Z" }, - { url = "https://files.pythonhosted.org/packages/ea/64/7e3023ee979ce6619c053c3c852aafea6f8448d67c9b047f9233ce179cd0/pycocotools-2.0.11-cp39-cp39-win_amd64.whl", hash = "sha256:eba35d6e06caaea28ce65a4b92e4343ecbb9abf4915f7ef7fca989b80839111c", size = 80520, upload-time = "2025-12-15T22:31:44.216Z" }, - { url = "https://files.pythonhosted.org/packages/d8/8b/f2a7a8fbae6bef8ea12428ecca6c208366a3ba79b2b76812cd6853f10f1b/pycocotools-2.0.11-cp39-cp39-win_arm64.whl", hash = "sha256:1192de413a23b4b94199197e8f8dbe1277cc24e6e9847bee6a71be3d8e543963", size = 69573, upload-time = "2025-12-15T22:31:45.189Z" }, -] - -[[package]] -name = "pycparser" -version = "2.23" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + { url = "https://files.pythonhosted.org/packages/63/3c/68d7ea376aada9046e7ea2d7d0dad0d27e1ae8b4b3c26a28346689390ab2/pycocotools-2.0.11-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fd4121766cc057133534679c0ec3f9023dbd96e9b31cf95c86a069ebdac2b65", size = 398434, upload-time = "2025-12-15T22:31:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/dc81895beff4e1207a829d40d442ea87cefaac9f6499151965f05c479619/pycocotools-2.0.11-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a82d1c9ed83f75da0b3f244f2a3cf559351a283307bd9b79a4ee2b93ab3231dd", size = 411685, upload-time = "2025-12-15T22:31:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0b/5a8a7de300862a2eb5e2ecd3cb015126231379206cd3ebba8f025388d770/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89e853425018e2c2920ee0f2112cf7c140a1dcf5f4f49abd9c2da112c3e0f4b3", size = 390500, upload-time = "2025-12-15T22:31:15.138Z" }, + { url = "https://files.pythonhosted.org/packages/63/b5/519bb68647f06feea03d5f355c33c05800aeae4e57b9482b2859eb00752e/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87af87b8d06d5b852a885a319d9362dca3bed9f8bbcc3feb6513acb1f88ea242", size = 409790, upload-time = "2025-12-15T22:31:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/83/b4/f6708404ff494706b80e714b919f76dc4ec9845a4007affd6d6b0843f928/pycocotools-2.0.11-cp312-abi3-win_amd64.whl", hash = "sha256:ffe806ce535f5996445188f9a35643791dc54beabc61bd81e2b03367356d604f", size = 77570, upload-time = "2025-12-15T22:31:17.703Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/778cd0ddc9d4a78915ac0a72b56d7fb204f7c3fabdad067d67ea0089762e/pycocotools-2.0.11-cp312-abi3-win_arm64.whl", hash = "sha256:c230f5e7b14bd19085217b4f40bba81bf14a182b150b8e9fab1c15d504ade343", size = 64564, upload-time = "2025-12-15T22:31:18.652Z" }, ] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, @@ -3653,56 +2425,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] -[[package]] -name = "pytest" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pluggy", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, -] - [[package]] name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pluggy", marker = "python_full_version >= '3.10'" }, - { name = "pygments", marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ @@ -3714,11 +2448,9 @@ name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, - { name = "coverage", version = "7.13.3", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ @@ -3741,9 +2473,6 @@ wheels = [ name = "python-json-logger" version = "4.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, @@ -3785,16 +2514,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/cb/ad2a16d6b20a457e8acd745dca416f19cf0de738311d213c544112260cc8/python_rapidjson-1.23-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad674edb9dfe8181fb704a14149e5eb30ae179a92021484ebe8935b8d0f88495", size = 2675144, upload-time = "2025-12-07T07:19:22.609Z" }, { url = "https://files.pythonhosted.org/packages/65/27/943fef83837f002d990274b82d5193d066aeef128c2ba6c009d549d0e5ad/python_rapidjson-1.23-cp312-cp312-win32.whl", hash = "sha256:0c64958048ce714ccc42c659ef954812ed6de79fe4800322b3926ca46f60ffd9", size = 130858, upload-time = "2025-12-07T07:19:23.887Z" }, { url = "https://files.pythonhosted.org/packages/89/cd/ef6c1bc784c3a081fabcf867c1b3affcb18ba1ffd9d71aa036f96a2ef979/python_rapidjson-1.23-cp312-cp312-win_amd64.whl", hash = "sha256:cbb0a67a5330d28279a5c3b68068e901deedcd21ade0ec23be1bcc250948ae62", size = 151270, upload-time = "2025-12-07T07:19:25.057Z" }, - { url = "https://files.pythonhosted.org/packages/10/d9/eae90129ee6c005887f2b1f1c14d1e4a75a1fcdad162c19c4c49690822f2/python_rapidjson-1.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:938d1bd7448151732aa7c70b251dbe9b7b3246a1b8b80c4110e86903e450a5f9", size = 215699, upload-time = "2025-12-07T07:20:07.851Z" }, - { url = "https://files.pythonhosted.org/packages/49/52/5735a53134d00242b747670d60493e3f0daea8eb1aae92561eda94108572/python_rapidjson-1.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:023ad77d11fd8023c03a038a4dbf734d6d54b74d9fa41414d93d0f696a77437d", size = 213043, upload-time = "2025-12-07T07:20:08.914Z" }, - { url = "https://files.pythonhosted.org/packages/81/c8/82b6f3c1e190e847ef42f721c564de9d94c7c068ad3caa8fce9aa5021119/python_rapidjson-1.23-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fe29411bbb63a15ea8c0003cbc82c185a4cf9602527d0fb026ccda10e440893", size = 1688385, upload-time = "2025-12-07T07:20:10.301Z" }, - { url = "https://files.pythonhosted.org/packages/d2/84/8cf37410060da7e481d64452a376d9fac3d910935a520901a5fb4f1b7237/python_rapidjson-1.23-cp39-cp39-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b90c2e51b3050062df91992c5fcdaf89def37450c41b067639a19d9aaa5fc85e", size = 1744729, upload-time = "2025-12-07T07:20:11.71Z" }, - { url = "https://files.pythonhosted.org/packages/a6/aa/1da7c2a9f5d6f9c840dd20af6f5410d30b67ddd3d860570820494be19a44/python_rapidjson-1.23-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8956da1444a45ee5c4596b1574c50d6dc09a0874f388b975b726ddd0c4068c16", size = 1724906, upload-time = "2025-12-07T07:20:13.597Z" }, - { url = "https://files.pythonhosted.org/packages/df/49/977ba93cd3a19e9b0ea13143607398646a970878a9835eae35949084ad3f/python_rapidjson-1.23-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:29270878291beb42b6ac9abead70323780a44494d3451f820ffd81b3812f603c", size = 2532845, upload-time = "2025-12-07T07:20:15.129Z" }, - { url = "https://files.pythonhosted.org/packages/0d/98/51cecda13f501d5c1de0b4531884e18622c32c5ddb32f4d5a409b8131a56/python_rapidjson-1.23-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:63c072656336a0046a4176dd2a961dc26fe81a80c39c5908561820763fe3e632", size = 2663792, upload-time = "2025-12-07T07:20:16.823Z" }, - { url = "https://files.pythonhosted.org/packages/75/c0/772fbb4a677b240446b70d73e4cfc4e93e37bae6d613f0d00939b766af88/python_rapidjson-1.23-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:71b0d9eb4529610dec6b23d21056be5a6d3fe143bef177f8434df3a647774716", size = 2645035, upload-time = "2025-12-07T07:20:18.152Z" }, - { url = "https://files.pythonhosted.org/packages/54/37/6499427f4f77ff5c7a4bc0237093b37cef7e8c1ccd0dd29a3292cb294894/python_rapidjson-1.23-cp39-cp39-win32.whl", hash = "sha256:f0ceef3cd234754cc224ef73fcd1e9faa88ea278c5c1a578301ab31816345243", size = 129751, upload-time = "2025-12-07T07:20:20.829Z" }, - { url = "https://files.pythonhosted.org/packages/50/1f/4c1f9d9a93a0d772c8ba3248a1c5fc67a10badcadced9e965c5c9a25e737/python_rapidjson-1.23-cp39-cp39-win_amd64.whl", hash = "sha256:d50ba0791f5b2d6525cc6d2bbc9f4dca5d9991271ca57c6d1a82d782ce67e31b", size = 151236, upload-time = "2025-12-07T07:20:22.253Z" }, ] [[package]] @@ -3820,9 +2539,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, ] [[package]] @@ -3837,7 +2553,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/1e/8a54166a8c5e4f5cb516514bdf4090be4d51a71e8d9f6d98c0aa00fe45d4/pywinpty-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc1e230e5b193eef4431cba3f39996a288f9958f9c9f092c8a961d930ee8f68", size = 236191, upload-time = "2026-02-04T21:50:36.239Z" }, { url = "https://files.pythonhosted.org/packages/7c/d4/aeb5e1784d2c5bff6e189138a9ca91a090117459cea0c30378e1f2db3d54/pywinpty-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c9081df0e49ffa86d15db4a6ba61530630e48707f987df42c9d3313537e81fc0", size = 2113098, upload-time = "2026-02-04T21:54:37.711Z" }, { url = "https://files.pythonhosted.org/packages/b9/53/7278223c493ccfe4883239cf06c823c56460a8010e0fc778eef67858dc14/pywinpty-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:15e79d870e18b678fb8a5a6105fd38496b55697c66e6fc0378236026bc4d59e9", size = 234901, upload-time = "2026-02-04T21:53:31.35Z" }, - { url = "https://files.pythonhosted.org/packages/d7/48/57c3d4e63aa93280ad1b1c2b3a5412d9d1fdee9955fa0aef43a84518d31b/pywinpty-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:0f10e81d52d7f2c4d927f645f247028e64eaf205a3ed9e64dbd998122108a218", size = 2116037, upload-time = "2026-02-04T21:53:56.58Z" }, ] [[package]] @@ -3874,15 +2589,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, - { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, - { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, - { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, - { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, ] [[package]] @@ -3924,16 +2630,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4e/782eb6df91b6a9d9afa96c2dcfc5cac62562a68eb62a02210101f886014d/pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb", size = 1330426, upload-time = "2025-09-08T23:09:21.03Z" }, - { url = "https://files.pythonhosted.org/packages/8d/ca/2b8693d06b1db4e0c084871e4c9d7842b561d0a6ff9d780640f5e3e9eb55/pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429", size = 906559, upload-time = "2025-09-08T23:09:22.983Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b3/b99b39e2cfdcebd512959780e4d299447fd7f46010b1d88d63324e2481ec/pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d", size = 863816, upload-time = "2025-09-08T23:09:24.556Z" }, - { url = "https://files.pythonhosted.org/packages/61/b2/018fa8e8eefb34a625b1a45e2effcbc9885645b22cdd0a68283f758351e7/pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345", size = 666735, upload-time = "2025-09-08T23:09:26.297Z" }, - { url = "https://files.pythonhosted.org/packages/01/05/8ae778f7cd7c94030731ae2305e6a38f3a333b6825f56c0c03f2134ccf1b/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968", size = 1655425, upload-time = "2025-09-08T23:09:28.172Z" }, - { url = "https://files.pythonhosted.org/packages/ad/ad/d69478a97a3f3142f9dbbbd9daa4fcf42541913a85567c36d4cfc19b2218/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098", size = 2033729, upload-time = "2025-09-08T23:09:30.097Z" }, - { url = "https://files.pythonhosted.org/packages/9a/6d/e3c6ad05bc1cddd25094e66cc15ae8924e15c67e231e93ed2955c401007e/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f", size = 1891803, upload-time = "2025-09-08T23:09:31.875Z" }, - { url = "https://files.pythonhosted.org/packages/7f/a7/97e8be0daaca157511563160b67a13d4fe76b195e3fa6873cb554ad46be3/pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78", size = 567627, upload-time = "2025-09-08T23:09:33.98Z" }, - { url = "https://files.pythonhosted.org/packages/5c/91/70bbf3a7c5b04c904261ef5ba224d8a76315f6c23454251bf5f55573a8a1/pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db", size = 632315, upload-time = "2025-09-08T23:09:36.097Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b5/a4173a83c7fd37f6bdb5a800ea338bc25603284e9ef8681377cec006ede4/pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc", size = 559833, upload-time = "2025-09-08T23:09:38.183Z" }, { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, @@ -3944,55 +2640,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, - { url = "https://files.pythonhosted.org/packages/57/f4/c2e978cf6b833708bad7d6396c3a20c19750585a1775af3ff13c435e1912/pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f", size = 836257, upload-time = "2025-09-08T23:10:07.635Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5f/4e10c7f57a4c92ab0fbb2396297aa8d618e6f5b9b8f8e9756d56f3e6fc52/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8", size = 800203, upload-time = "2025-09-08T23:10:09.436Z" }, - { url = "https://files.pythonhosted.org/packages/19/72/a74a007cd636f903448c6ab66628104b1fc5f2ba018733d5eabb94a0a6fb/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381", size = 758756, upload-time = "2025-09-08T23:10:11.733Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d4/30c25b91f2b4786026372f5ef454134d7f576fcf4ac58539ad7dd5de4762/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172", size = 567742, upload-time = "2025-09-08T23:10:14.732Z" }, - { url = "https://files.pythonhosted.org/packages/92/aa/ee86edad943438cd0316964020c4b6d09854414f9f945f8e289ea6fcc019/pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9", size = 544857, upload-time = "2025-09-08T23:10:16.431Z" }, -] - -[[package]] -name = "referencing" -version = "0.36.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "attrs", marker = "python_full_version < '3.10'" }, - { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, ] [[package]] name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "attrs", marker = "python_full_version >= '3.10'" }, - { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ @@ -4054,23 +2711,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/a2/e7/0e1913dc52eee9c5cf8417c9813c4c55972a3f37d27cfa2e623b79b63dbc/regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5", size = 488185, upload-time = "2026-01-14T23:17:25.2Z" }, - { url = "https://files.pythonhosted.org/packages/78/df/c52c1ff4221529faad0953e197982fe9508c6dbb42327e31bf98ea07472a/regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815", size = 290628, upload-time = "2026-01-14T23:17:27.125Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d2/a2fef3717deaff647d7de2bccf899a576c7eaf042b6b271fc4474515fe97/regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b", size = 288509, upload-time = "2026-01-14T23:17:29.017Z" }, - { url = "https://files.pythonhosted.org/packages/70/89/faf5ee5c69168753c845a3d58b4683f61c899d162bfe1264fca88d5b3924/regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6", size = 781088, upload-time = "2026-01-14T23:17:30.961Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2c/707e5c380ad547c93686e21144e7e24dc2064dd84ec5b751b6dbdfc9be2b/regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b", size = 850516, upload-time = "2026-01-14T23:17:32.946Z" }, - { url = "https://files.pythonhosted.org/packages/5d/3b/baa816cdcad1c0f8195f9f40ab2b2a2246c8a2989dcd90641c0c6559e3fd/regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df", size = 898124, upload-time = "2026-01-14T23:17:36.019Z" }, - { url = "https://files.pythonhosted.org/packages/e7/74/1eb46bde30899825ed9fdf645eba16b7b97c49d12d300f5177989b9a09a4/regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e", size = 791290, upload-time = "2026-01-14T23:17:38.097Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5d/b72e176fb21e2ec248baed01151a342d1f44dd43c2b6bb6a41ad183b274e/regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c", size = 781996, upload-time = "2026-01-14T23:17:40.109Z" }, - { url = "https://files.pythonhosted.org/packages/61/0e/d3b3710eaafd994a4a71205d114abc38cda8691692a2ce2313abe68e7eb7/regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839", size = 767578, upload-time = "2026-01-14T23:17:42.134Z" }, - { url = "https://files.pythonhosted.org/packages/09/51/c6a6311833e040f95d229a34d82ac1cec2af8a5c00d58b244f2fceecef87/regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c", size = 774354, upload-time = "2026-01-14T23:17:44.392Z" }, - { url = "https://files.pythonhosted.org/packages/cc/97/c522d1f19fb2c549aaf680b115c110cd124c02062bc8c95f33db8583b4bb/regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077", size = 845297, upload-time = "2026-01-14T23:17:47.145Z" }, - { url = "https://files.pythonhosted.org/packages/99/a0/99468c386ab68a5e24c946c5c353c29c33a95523e275c17839f2446db15d/regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4", size = 755132, upload-time = "2026-01-14T23:17:49.796Z" }, - { url = "https://files.pythonhosted.org/packages/70/33/d5748c7b6c9d3621f12570583561ba529e2d1b12e4f70b8f17979b133e65/regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a", size = 835662, upload-time = "2026-01-14T23:17:52.559Z" }, - { url = "https://files.pythonhosted.org/packages/ad/15/1986972c276672505437f1ba3c9706c2d91f321cfb9b2f4d06e8bff1b999/regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3", size = 779513, upload-time = "2026-01-14T23:17:54.711Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f9/124f6a5cb3969d8e30471ed4f46cfc17c47aef1a9863ee8b4ba1d98b1bc4/regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a", size = 265923, upload-time = "2026-01-14T23:17:56.69Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c2/bb8fad7d27f1d71fc9772befd544bccd22eddc62a6735f57b003b4aff005/regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc", size = 277900, upload-time = "2026-01-14T23:17:58.72Z" }, - { url = "https://files.pythonhosted.org/packages/f7/fa/4e033327c1d8350bc812cac906d873984d3d4b39529252f392a47ccc356d/regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5", size = 270413, upload-time = "2026-01-14T23:18:00.764Z" }, ] [[package]] @@ -4121,134 +2761,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, ] -[[package]] -name = "rpds-py" -version = "0.27.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" }, - { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" }, - { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" }, - { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" }, - { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" }, - { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" }, - { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" }, - { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" }, - { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" }, - { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" }, - { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" }, - { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" }, - { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" }, - { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" }, - { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" }, - { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" }, - { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, - { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, - { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, - { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, - { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, - { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, - { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, - { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, - { url = "https://files.pythonhosted.org/packages/7f/6c/252e83e1ce7583c81f26d1d884b2074d40a13977e1b6c9c50bbf9a7f1f5a/rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527", size = 372140, upload-time = "2025-08-27T12:15:05.441Z" }, - { url = "https://files.pythonhosted.org/packages/9d/71/949c195d927c5aeb0d0629d329a20de43a64c423a6aa53836290609ef7ec/rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d", size = 354086, upload-time = "2025-08-27T12:15:07.404Z" }, - { url = "https://files.pythonhosted.org/packages/9f/02/e43e332ad8ce4f6c4342d151a471a7f2900ed1d76901da62eb3762663a71/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8", size = 382117, upload-time = "2025-08-27T12:15:09.275Z" }, - { url = "https://files.pythonhosted.org/packages/d0/05/b0fdeb5b577197ad72812bbdfb72f9a08fa1e64539cc3940b1b781cd3596/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc", size = 394520, upload-time = "2025-08-27T12:15:10.727Z" }, - { url = "https://files.pythonhosted.org/packages/67/1f/4cfef98b2349a7585181e99294fa2a13f0af06902048a5d70f431a66d0b9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1", size = 522657, upload-time = "2025-08-27T12:15:12.613Z" }, - { url = "https://files.pythonhosted.org/packages/44/55/ccf37ddc4c6dce7437b335088b5ca18da864b334890e2fe9aa6ddc3f79a9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125", size = 402967, upload-time = "2025-08-27T12:15:14.113Z" }, - { url = "https://files.pythonhosted.org/packages/74/e5/5903f92e41e293b07707d5bf00ef39a0eb2af7190aff4beaf581a6591510/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905", size = 384372, upload-time = "2025-08-27T12:15:15.842Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e3/fbb409e18aeefc01e49f5922ac63d2d914328430e295c12183ce56ebf76b/rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e", size = 401264, upload-time = "2025-08-27T12:15:17.388Z" }, - { url = "https://files.pythonhosted.org/packages/55/79/529ad07794e05cb0f38e2f965fc5bb20853d523976719400acecc447ec9d/rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e", size = 418691, upload-time = "2025-08-27T12:15:19.144Z" }, - { url = "https://files.pythonhosted.org/packages/33/39/6554a7fd6d9906fda2521c6d52f5d723dca123529fb719a5b5e074c15e01/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786", size = 558989, upload-time = "2025-08-27T12:15:21.087Z" }, - { url = "https://files.pythonhosted.org/packages/19/b2/76fa15173b6f9f445e5ef15120871b945fb8dd9044b6b8c7abe87e938416/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec", size = 589835, upload-time = "2025-08-27T12:15:22.696Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/5560a4b39bab780405bed8a88ee85b30178061d189558a86003548dea045/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b", size = 555227, upload-time = "2025-08-27T12:15:24.278Z" }, - { url = "https://files.pythonhosted.org/packages/52/d7/cd9c36215111aa65724c132bf709c6f35175973e90b32115dedc4ced09cb/rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52", size = 217899, upload-time = "2025-08-27T12:15:25.926Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e0/d75ab7b4dd8ba777f6b365adbdfc7614bbfe7c5f05703031dfa4b61c3d6c/rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab", size = 228725, upload-time = "2025-08-27T12:15:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" }, - { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" }, - { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" }, - { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" }, - { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" }, - { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" }, - { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" }, - { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" }, - { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" }, - { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" }, - { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" }, - { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" }, - { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" }, - { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" }, - { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" }, - { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" }, - { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ea/5463cd5048a7a2fcdae308b6e96432802132c141bfb9420260142632a0f1/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475", size = 371778, upload-time = "2025-08-27T12:16:13.851Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/f38c099db07f5114029c1467649d308543906933eebbc226d4527a5f4693/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f", size = 354394, upload-time = "2025-08-27T12:16:15.609Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/b76f97704d9dd8ddbd76fed4c4048153a847c5d6003afe20a6b5c3339065/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6", size = 382348, upload-time = "2025-08-27T12:16:17.251Z" }, - { url = "https://files.pythonhosted.org/packages/8a/3f/ef23d3c1be1b837b648a3016d5bbe7cfe711422ad110b4081c0a90ef5a53/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3", size = 394159, upload-time = "2025-08-27T12:16:19.251Z" }, - { url = "https://files.pythonhosted.org/packages/74/8a/9e62693af1a34fd28b1a190d463d12407bd7cf561748cb4745845d9548d3/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3", size = 522775, upload-time = "2025-08-27T12:16:20.929Z" }, - { url = "https://files.pythonhosted.org/packages/36/0d/8d5bb122bf7a60976b54c5c99a739a3819f49f02d69df3ea2ca2aff47d5c/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8", size = 402633, upload-time = "2025-08-27T12:16:22.548Z" }, - { url = "https://files.pythonhosted.org/packages/0f/0e/237948c1f425e23e0cf5a566d702652a6e55c6f8fbd332a1792eb7043daf/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400", size = 384867, upload-time = "2025-08-27T12:16:24.29Z" }, - { url = "https://files.pythonhosted.org/packages/d6/0a/da0813efcd998d260cbe876d97f55b0f469ada8ba9cbc47490a132554540/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485", size = 401791, upload-time = "2025-08-27T12:16:25.954Z" }, - { url = "https://files.pythonhosted.org/packages/51/78/c6c9e8a8aaca416a6f0d1b6b4a6ee35b88fe2c5401d02235d0a056eceed2/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1", size = 419525, upload-time = "2025-08-27T12:16:27.659Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/5af37e1d71487cf6d56dd1420dc7e0c2732c1b6ff612aa7a88374061c0a8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5", size = 559255, upload-time = "2025-08-27T12:16:29.343Z" }, - { url = "https://files.pythonhosted.org/packages/40/7f/8b7b136069ef7ac3960eda25d832639bdb163018a34c960ed042dd1707c8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4", size = 590384, upload-time = "2025-08-27T12:16:31.005Z" }, - { url = "https://files.pythonhosted.org/packages/d8/06/c316d3f6ff03f43ccb0eba7de61376f8ec4ea850067dddfafe98274ae13c/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c", size = 555959, upload-time = "2025-08-27T12:16:32.73Z" }, - { url = "https://files.pythonhosted.org/packages/60/94/384cf54c430b9dac742bbd2ec26c23feb78ded0d43d6d78563a281aec017/rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859", size = 228784, upload-time = "2025-08-27T12:16:34.428Z" }, -] - [[package]] name = "rpds-py" version = "0.30.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, @@ -4330,11 +2846,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/91/e04c66fc6b02a0c2cea519f9d417bbf8feea67f1f438ffc2ce5c99fcb4ea/ruff_api-0.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a144747e3583cf476f1509f2733969fff3dbf73b08725aa4fdf7a385f3d2238b", size = 5019318, upload-time = "2024-10-25T04:51:07.929Z" }, { url = "https://files.pythonhosted.org/packages/19/5d/fa705a70d1a338751df15a1a7b30efab8ac86e89207b4977ff065ae6c01b/ruff_api-0.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4412c2e9fccefe33ff5f8c99749995e9d56a324d2d107c805c8a1d9e5c9e229a", size = 5233200, upload-time = "2024-10-25T04:51:10.266Z" }, { url = "https://files.pythonhosted.org/packages/24/2a/22560db538635f98046e0d338ead76d66c5019250060eb8964156c9eef01/ruff_api-0.1.0-cp312-none-win_amd64.whl", hash = "sha256:647456e1d24adf2809b120fb299e2908a59da789462c30eefd917ac8251e7643", size = 4481760, upload-time = "2024-10-25T04:51:12.284Z" }, - { url = "https://files.pythonhosted.org/packages/b3/31/95419e6fe45c17e6239c7b5de7d69e55cc0c62e8caee8b6aa43ba8d3ab37/ruff_api-0.1.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e1d8947933df35503ee15ebee88dabc3d537f5a8bf9cccd9127fa14b4821ecfb", size = 4737000, upload-time = "2024-10-25T04:51:32.731Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e3/abdba21b9344423b7b56334b54e2a8c3f0191142e9facbd657bfcb375c60/ruff_api-0.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:def7d080c65f37233412b4c6188a90d7a91abc1dbcb171f82ebf56290a9349c5", size = 4534540, upload-time = "2024-10-25T04:51:34.343Z" }, - { url = "https://files.pythonhosted.org/packages/0a/9e/848a044d44d0ae72092854ca338adb322080ed3a1cb2999262147c9da811/ruff_api-0.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42ff87c5273ec3a1797353b0f6bc6384dfbc8f67ce2a03a1bc11c6a5d5ea47c9", size = 5020144, upload-time = "2024-10-25T04:51:35.891Z" }, - { url = "https://files.pythonhosted.org/packages/68/ae/f0c53dd332f359974b9e3a4aa7ebfa2c8dfc2cf9fc8ab750da912bb28863/ruff_api-0.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1ef6a9ade72e83088de8e2c36918c1f8668a53fba521a4617d0f8b1b8531262", size = 5233958, upload-time = "2024-10-25T04:51:38.109Z" }, - { url = "https://files.pythonhosted.org/packages/e6/73/c93554b33c95e68db61b7ad39d6ea6d10560143a60f2e0154c53074243fc/ruff_api-0.1.0-cp39-none-win_amd64.whl", hash = "sha256:9c17ce23f995b07977c1b7a7720ccff6c4b8c273009c6cc0ffc6819692e44fb6", size = 4481808, upload-time = "2024-10-25T04:51:39.791Z" }, ] [[package]] @@ -4361,10 +2872,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, - { url = "https://files.pythonhosted.org/packages/94/60/13ccb63ea85bfe2e4fe6af602cf1272155f048906556d5ec8509da9dba42/safetensors-0.7.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b95a3fa7b3abb9b5b0e07668e808364d0d40f6bbbf9ae0faa8b5b210c97b140", size = 492627, upload-time = "2025-11-19T15:18:14.661Z" }, - { url = "https://files.pythonhosted.org/packages/2e/2b/e2fde0d6334439908b0b0c4cba18b8ad76ea6a03b569d4a3388f423b4046/safetensors-0.7.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cfdead2f57330d76aa7234051dadfa7d4eedc0e5a27fd08e6f96714a92b00f09", size = 503861, upload-time = "2025-11-19T15:18:19.418Z" }, - { url = "https://files.pythonhosted.org/packages/f0/71/566e3dd559a9cef1b4775c239daae09e6b6a32ca8b45eb1db9a4dfa1ba81/safetensors-0.7.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc92bc2db7b45bda4510e4f51c59b00fe80b2d6be88928346e4294ce1c2abe7c", size = 623577, upload-time = "2025-11-19T15:18:24.275Z" }, - { url = "https://files.pythonhosted.org/packages/82/fc/3035c5c30c8a5a82c31c6b2ad6f8bcd45ea2ddd9a8088840406bcf997413/safetensors-0.7.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6999421eb8ba9df4450a16d9184fcb7bef26240b9f98e95401f17af6c2210b71", size = 532524, upload-time = "2025-11-19T15:18:29.334Z" }, ] [[package]] @@ -4379,6 +2886,8 @@ dependencies = [ { name = "psutil" }, { name = "regex" }, { name = "timm" }, + { name = "torch" }, + { name = "torchvision" }, { name = "tqdm" }, { name = "typing-extensions" }, ] @@ -4387,21 +2896,18 @@ dependencies = [ dev = [ { name = "black" }, { name = "gitpython" }, - { name = "numba", version = "0.60.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numba", version = "0.63.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numba" }, { name = "opencv-python" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pycocotools" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, { name = "pytest-cov" }, { name = "python-rapidjson" }, { name = "ruff-api" }, { name = "ufmt" }, { name = "usort" }, - { name = "yt-dlp", version = "2025.10.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "yt-dlp", version = "2026.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "yt-dlp" }, ] notebooks = [ { name = "decord" }, @@ -4410,30 +2916,24 @@ notebooks = [ { name = "ipympl" }, { name = "ipywidgets" }, { name = "jupyter" }, - { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "matplotlib" }, { name = "notebook" }, { name = "opencv-python" }, { name = "pycocotools" }, - { name = "scikit-image", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scikit-learn", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] train = [ { name = "fairscale" }, { name = "fvcore" }, { name = "hydra-core" }, - { name = "scikit-image", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scikit-learn", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "submitit" }, { name = "tensorboard" }, @@ -4445,21 +2945,18 @@ train = [ dev = [ { name = "black" }, { name = "gitpython" }, - { name = "numba", version = "0.60.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numba", version = "0.63.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numba" }, { name = "opencv-python" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pycocotools" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, { name = "pytest-cov" }, { name = "python-rapidjson" }, { name = "ruff-api" }, { name = "ufmt" }, { name = "usort" }, - { name = "yt-dlp", version = "2025.10.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "yt-dlp", version = "2026.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "yt-dlp" }, ] [package.metadata] @@ -4502,7 +2999,9 @@ requires-dist = [ { name = "submitit", marker = "extra == 'train'" }, { name = "tensorboard", marker = "extra == 'train'" }, { name = "timm", specifier = ">=1.0.17" }, + { name = "torch", specifier = ">=2.11,<2.12" }, { name = "torchmetrics", marker = "extra == 'train'" }, + { name = "torchvision", specifier = ">=0.26,<0.27" }, { name = "tqdm" }, { name = "typing-extensions" }, { name = "ufmt", marker = "extra == 'dev'", specifier = "==2.8.0" }, @@ -4529,67 +3028,24 @@ dev = [ { name = "yt-dlp" }, ] -[[package]] -name = "scikit-image" -version = "0.24.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "imageio", marker = "python_full_version < '3.10'" }, - { name = "lazy-loader", marker = "python_full_version < '3.10'" }, - { name = "networkx", version = "3.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "tifffile", version = "2024.8.30", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5d/c5/bcd66bf5aae5587d3b4b69c74bee30889c46c9778e858942ce93a030e1f3/scikit_image-0.24.0.tar.gz", hash = "sha256:5d16efe95da8edbeb363e0c4157b99becbd650a60b77f6e3af5768b66cf007ab", size = 22693928, upload-time = "2024-06-18T19:05:31.49Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/82/d4eaa6e441f28a783762093a3c74bcc4a67f1c65bf011414ad4ea85187d8/scikit_image-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb3bc0264b6ab30b43c4179ee6156bc18b4861e78bb329dd8d16537b7bbf827a", size = 14051470, upload-time = "2024-06-18T19:03:37.385Z" }, - { url = "https://files.pythonhosted.org/packages/65/15/1879307aaa2c771aa8ef8f00a171a85033bffc6b2553cfd2657426881452/scikit_image-0.24.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:9c7a52e20cdd760738da38564ba1fed7942b623c0317489af1a598a8dedf088b", size = 13385822, upload-time = "2024-06-18T19:03:43.996Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b8/2d52864714b82122f4a36f47933f61f1cd2a6df34987873837f8064d4fdf/scikit_image-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93f46e6ce42e5409f4d09ce1b0c7f80dd7e4373bcec635b6348b63e3c886eac8", size = 14216787, upload-time = "2024-06-18T19:03:50.169Z" }, - { url = "https://files.pythonhosted.org/packages/40/2e/8b39cd2c347490dbe10adf21fd50bbddb1dada5bb0512c3a39371285eb62/scikit_image-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39ee0af13435c57351a3397eb379e72164ff85161923eec0c38849fecf1b4764", size = 14866533, upload-time = "2024-06-18T19:03:56.286Z" }, - { url = "https://files.pythonhosted.org/packages/99/89/3fcd68d034db5d29c974e964d03deec9d0fbf9410ff0a0b95efff70947f6/scikit_image-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ac7913b028b8aa780ffae85922894a69e33d1c0bf270ea1774f382fe8bf95e7", size = 12864601, upload-time = "2024-06-18T19:04:00.868Z" }, - { url = "https://files.pythonhosted.org/packages/90/e3/564beb0c78bf83018a146dfcdc959c99c10a0d136480b932a350c852adbc/scikit_image-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:272909e02a59cea3ed4aa03739bb88df2625daa809f633f40b5053cf09241831", size = 14020429, upload-time = "2024-06-18T19:04:07.18Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f6/be8b16d8ab6ebf19057877c2aec905cbd438dd92ca64b8efe9e9af008fa3/scikit_image-0.24.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:190ebde80b4470fe8838764b9b15f232a964f1a20391663e31008d76f0c696f7", size = 13371950, upload-time = "2024-06-18T19:04:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/b8/2e/3a949995f8fc2a65b15a4964373e26c5601cb2ea68f36b115571663e7a38/scikit_image-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c98cc695005faf2b79904e4663796c977af22586ddf1b12d6af2fa22842dc2", size = 14197889, upload-time = "2024-06-18T19:04:17.181Z" }, - { url = "https://files.pythonhosted.org/packages/ad/96/138484302b8ec9a69cdf65e8d4ab47a640a3b1a8ea3c437e1da3e1a5a6b8/scikit_image-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa27b3a0dbad807b966b8db2d78da734cb812ca4787f7fbb143764800ce2fa9c", size = 14861425, upload-time = "2024-06-18T19:04:27.363Z" }, - { url = "https://files.pythonhosted.org/packages/50/b2/d5e97115733e2dc657e99868ae0237705b79d0c81f6ced21b8f0799a30d1/scikit_image-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:dacf591ac0c272a111181afad4b788a27fe70d213cfddd631d151cbc34f8ca2c", size = 12843506, upload-time = "2024-06-18T19:04:35.782Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/45ad3b8b8ab8d275a48a9d1016c4beb1c2801a7a13e384268861d01145c1/scikit_image-0.24.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6fccceb54c9574590abcddc8caf6cefa57c13b5b8b4260ab3ff88ad8f3c252b3", size = 14101823, upload-time = "2024-06-18T19:04:39.576Z" }, - { url = "https://files.pythonhosted.org/packages/6e/75/db10ee1bc7936b411d285809b5fe62224bbb1b324a03dd703582132ce5ee/scikit_image-0.24.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ccc01e4760d655aab7601c1ba7aa4ddd8b46f494ac46ec9c268df6f33ccddf4c", size = 13420758, upload-time = "2024-06-18T19:04:45.645Z" }, - { url = "https://files.pythonhosted.org/packages/87/fd/07a7396962abfe22a285a922a63d18e4d5ec48eb5dbb1c06e96fb8fb6528/scikit_image-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18836a18d3a7b6aca5376a2d805f0045826bc6c9fc85331659c33b4813e0b563", size = 14256813, upload-time = "2024-06-18T19:04:51.68Z" }, - { url = "https://files.pythonhosted.org/packages/2c/24/4bcd94046b409ac4d63e2f92e46481f95f5006a43e68f6ab2b24f5d70ab4/scikit_image-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8579bda9c3f78cb3b3ed8b9425213c53a25fa7e994b7ac01f2440b395babf660", size = 15013039, upload-time = "2024-06-18T19:04:56.433Z" }, - { url = "https://files.pythonhosted.org/packages/d9/17/b561823143eb931de0f82fed03ae128ef954a9641309602ea0901c357f95/scikit_image-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:82ab903afa60b2da1da2e6f0c8c65e7c8868c60a869464c41971da929b3e82bc", size = 12949363, upload-time = "2024-06-18T19:05:02.773Z" }, - { url = "https://files.pythonhosted.org/packages/93/8e/b6e50d8a6572daf12e27acbf9a1722fdb5e6bfc64f04a5fefa2a71fea0c3/scikit_image-0.24.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef04360eda372ee5cd60aebe9be91258639c86ae2ea24093fb9182118008d009", size = 14083010, upload-time = "2024-06-18T19:05:07.582Z" }, - { url = "https://files.pythonhosted.org/packages/d6/6c/f528c6b80b4e9d38444d89f0d1160797d20c640b7a8cabd8b614ac600b79/scikit_image-0.24.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e9aadb442360a7e76f0c5c9d105f79a83d6df0e01e431bd1d5757e2c5871a1f3", size = 13414235, upload-time = "2024-06-18T19:05:11.58Z" }, - { url = "https://files.pythonhosted.org/packages/52/03/59c52aa59b952aafcf19163e5d7e924e6156c3d9e9c86ea3372ad31d90f8/scikit_image-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e37de6f4c1abcf794e13c258dc9b7d385d5be868441de11c180363824192ff7", size = 14238540, upload-time = "2024-06-18T19:05:17.481Z" }, - { url = "https://files.pythonhosted.org/packages/f0/cc/1a58efefb9b17c60d15626b33416728003028d5d51f0521482151a222560/scikit_image-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4688c18bd7ec33c08d7bf0fd19549be246d90d5f2c1d795a89986629af0a1e83", size = 14883801, upload-time = "2024-06-18T19:05:23.231Z" }, - { url = "https://files.pythonhosted.org/packages/9d/63/233300aa76c65a442a301f9d2416a9b06c91631287bd6dd3d6b620040096/scikit_image-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:56dab751d20b25d5d3985e95c9b4e975f55573554bd76b0aedf5875217c93e69", size = 12891952, upload-time = "2024-06-18T19:05:27.173Z" }, -] - [[package]] name = "scikit-image" version = "0.25.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "imageio", marker = "python_full_version == '3.10.*'" }, - { name = "lazy-loader", marker = "python_full_version == '3.10.*'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "numpy", marker = "python_full_version == '3.10.*'" }, - { name = "packaging", marker = "python_full_version == '3.10.*'" }, - { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "imageio", marker = "python_full_version < '3.11'" }, + { name = "lazy-loader", marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pillow", marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } wheels = [ @@ -4632,7 +3088,7 @@ dependencies = [ { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", marker = "python_full_version >= '3.11'" }, { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow", marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tifffile", version = "2026.1.28", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] @@ -4656,59 +3112,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/35/8a/d1b8055f584acc937478abf4550d122936f420352422a1a625eef2c605d8/scikit_image-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8", size = 11348740, upload-time = "2025-12-20T17:11:09.118Z" }, ] -[[package]] -name = "scikit-learn" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "joblib", marker = "python_full_version < '3.10'" }, - { name = "numpy", marker = "python_full_version < '3.10'" }, - { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "threadpoolctl", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312, upload-time = "2025-01-10T08:07:55.348Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/3a/f4597eb41049110b21ebcbb0bcb43e4035017545daa5eedcfeb45c08b9c5/scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e", size = 12067702, upload-time = "2025-01-10T08:05:56.515Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/0423e5e1fd1c6ec5be2352ba05a537a473c1677f8188b9306097d684b327/scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36", size = 11112765, upload-time = "2025-01-10T08:06:00.272Z" }, - { url = "https://files.pythonhosted.org/packages/70/95/d5cb2297a835b0f5fc9a77042b0a2d029866379091ab8b3f52cc62277808/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5", size = 12643991, upload-time = "2025-01-10T08:06:04.813Z" }, - { url = "https://files.pythonhosted.org/packages/b7/91/ab3c697188f224d658969f678be86b0968ccc52774c8ab4a86a07be13c25/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b", size = 13497182, upload-time = "2025-01-10T08:06:08.42Z" }, - { url = "https://files.pythonhosted.org/packages/17/04/d5d556b6c88886c092cc989433b2bab62488e0f0dafe616a1d5c9cb0efb1/scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002", size = 11125517, upload-time = "2025-01-10T08:06:12.783Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/e291c29670795406a824567d1dfc91db7b699799a002fdaa452bceea8f6e/scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33", size = 12102620, upload-time = "2025-01-10T08:06:16.675Z" }, - { url = "https://files.pythonhosted.org/packages/25/92/ee1d7a00bb6b8c55755d4984fd82608603a3cc59959245068ce32e7fb808/scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d", size = 11116234, upload-time = "2025-01-10T08:06:21.83Z" }, - { url = "https://files.pythonhosted.org/packages/30/cd/ed4399485ef364bb25f388ab438e3724e60dc218c547a407b6e90ccccaef/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2", size = 12592155, upload-time = "2025-01-10T08:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8", size = 13497069, upload-time = "2025-01-10T08:06:32.515Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/c5b78606743a1f28eae8f11973de6613a5ee87366796583fb74c67d54939/scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415", size = 11139809, upload-time = "2025-01-10T08:06:35.514Z" }, - { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516, upload-time = "2025-01-10T08:06:40.009Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837, upload-time = "2025-01-10T08:06:43.305Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728, upload-time = "2025-01-10T08:06:47.618Z" }, - { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700, upload-time = "2025-01-10T08:06:50.888Z" }, - { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613, upload-time = "2025-01-10T08:06:54.115Z" }, - { url = "https://files.pythonhosted.org/packages/d2/37/b305b759cc65829fe1b8853ff3e308b12cdd9d8884aa27840835560f2b42/scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1", size = 12101868, upload-time = "2025-01-10T08:07:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/83/74/f64379a4ed5879d9db744fe37cfe1978c07c66684d2439c3060d19a536d8/scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e", size = 11144062, upload-time = "2025-01-10T08:07:37.67Z" }, - { url = "https://files.pythonhosted.org/packages/fd/dc/d5457e03dc9c971ce2b0d750e33148dd060fefb8b7dc71acd6054e4bb51b/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107", size = 12693173, upload-time = "2025-01-10T08:07:42.713Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/b1d2188967c3204c78fa79c9263668cf1b98060e8e58d1a730fe5b2317bb/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422", size = 13518605, upload-time = "2025-01-10T08:07:46.551Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d8/8d603bdd26601f4b07e2363032b8565ab82eb857f93d86d0f7956fcf4523/scikit_learn-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b", size = 11155078, upload-time = "2025-01-10T08:07:51.376Z" }, -] - [[package]] name = "scikit-learn" version = "1.7.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "joblib", marker = "python_full_version == '3.10.*'" }, - { name = "numpy", marker = "python_full_version == '3.10.*'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "threadpoolctl", marker = "python_full_version == '3.10.*'" }, + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -4767,57 +3184,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, ] -[[package]] -name = "scipy" -version = "1.13.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720, upload-time = "2024-05-23T03:29:26.079Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076, upload-time = "2024-05-23T03:19:01.687Z" }, - { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232, upload-time = "2024-05-23T03:19:09.089Z" }, - { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202, upload-time = "2024-05-23T03:19:15.138Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335, upload-time = "2024-05-23T03:19:21.984Z" }, - { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728, upload-time = "2024-05-23T03:19:28.225Z" }, - { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588, upload-time = "2024-05-23T03:19:35.661Z" }, - { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805, upload-time = "2024-05-23T03:19:43.081Z" }, - { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687, upload-time = "2024-05-23T03:19:48.799Z" }, - { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638, upload-time = "2024-05-23T03:19:55.104Z" }, - { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931, upload-time = "2024-05-23T03:20:01.82Z" }, - { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145, upload-time = "2024-05-23T03:20:09.173Z" }, - { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227, upload-time = "2024-05-23T03:20:16.433Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301, upload-time = "2024-05-23T03:20:23.538Z" }, - { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348, upload-time = "2024-05-23T03:20:29.885Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062, upload-time = "2024-05-23T03:20:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311, upload-time = "2024-05-23T03:20:42.086Z" }, - { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493, upload-time = "2024-05-23T03:20:48.292Z" }, - { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955, upload-time = "2024-05-23T03:20:55.091Z" }, - { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927, upload-time = "2024-05-23T03:21:01.95Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538, upload-time = "2024-05-23T03:21:07.634Z" }, - { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190, upload-time = "2024-05-23T03:21:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244, upload-time = "2024-05-23T03:21:21.827Z" }, - { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637, upload-time = "2024-05-23T03:21:28.729Z" }, - { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440, upload-time = "2024-05-23T03:21:35.888Z" }, -] - [[package]] name = "scipy" version = "1.15.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "numpy", marker = "python_full_version == '3.10.*'" }, + { name = "numpy", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -5009,15 +3386,12 @@ name = "tensorboard" version = "2.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "absl-py", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "absl-py", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "absl-py" }, { name = "grpcio" }, - { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "markdown", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown" }, { name = "numpy" }, { name = "packaging" }, - { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pillow" }, { name = "protobuf" }, { name = "setuptools" }, { name = "tensorboard-data-server" }, @@ -5037,39 +3411,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" }, ] -[[package]] -name = "termcolor" -version = "3.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" }, -] - [[package]] name = "termcolor" version = "3.3.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, @@ -5081,7 +3426,7 @@ version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess", marker = "os_name != 'nt'" }, - { name = "pywinpty", marker = "(python_full_version < '3.10' and os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "tornado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } @@ -5098,34 +3443,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] -[[package]] -name = "tifffile" -version = "2024.8.30" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/54/30/7017e5560154c100cad3a801c02adb48879cd8e8cb862b82696d84187184/tifffile-2024.8.30.tar.gz", hash = "sha256:2c9508fe768962e30f87def61819183fb07692c258cb175b3c114828368485a4", size = 365714, upload-time = "2024-08-31T17:32:43.945Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/4f/73714b1c1d339b1545cac28764e39f88c69468b5e10e51f327f9aa9d55b9/tifffile-2024.8.30-py3-none-any.whl", hash = "sha256:8bc59a8f02a2665cd50a910ec64961c5373bee0b8850ec89d3b7b485bf7be7ad", size = 227262, upload-time = "2024-08-31T17:32:41.87Z" }, -] - [[package]] name = "tifffile" version = "2025.5.10" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "numpy", marker = "python_full_version == '3.10.*'" }, + { name = "numpy", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" } wheels = [ @@ -5164,10 +3492,8 @@ dependencies = [ { name = "huggingface-hub" }, { name = "pyyaml" }, { name = "safetensors" }, - { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "torchvision", version = "0.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "torch" }, + { name = "torchvision" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f4/9d/0ea45640be447445c8664ce2b10c74f763b0b0b9ed11620d41a4d4baa10c/timm-1.0.24.tar.gz", hash = "sha256:c7b909f43fe2ef8fe62c505e270cd4f1af230dfbc37f2ee93e3608492b9d9a40", size = 2412239, upload-time = "2026-01-07T00:26:17.541Z" } wheels = [ @@ -5233,114 +3559,38 @@ wheels = [ [[package]] name = "torch" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jinja2", marker = "python_full_version < '3.10'" }, - { name = "networkx", version = "3.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "nvidia-cublas-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", version = "2.27.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "sympy", marker = "python_full_version < '3.10'" }, - { name = "triton", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/28/110f7274254f1b8476c561dada127173f994afa2b1ffc044efb773c15650/torch-2.8.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:0be92c08b44009d4131d1ff7a8060d10bafdb7ddcb7359ef8d8c5169007ea905", size = 102052793, upload-time = "2025-08-06T14:53:15.852Z" }, - { url = "https://files.pythonhosted.org/packages/70/1c/58da560016f81c339ae14ab16c98153d51c941544ae568da3cb5b1ceb572/torch-2.8.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:89aa9ee820bb39d4d72b794345cccef106b574508dd17dbec457949678c76011", size = 888025420, upload-time = "2025-08-06T14:54:18.014Z" }, - { url = "https://files.pythonhosted.org/packages/70/87/f69752d0dd4ba8218c390f0438130c166fa264a33b7025adb5014b92192c/torch-2.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e8e5bf982e87e2b59d932769938b698858c64cc53753894be25629bdf5cf2f46", size = 241363614, upload-time = "2025-08-06T14:53:31.496Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d6/e6d4c57e61c2b2175d3aafbfb779926a2cfd7c32eeda7c543925dceec923/torch-2.8.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a3f16a58a9a800f589b26d47ee15aca3acf065546137fc2af039876135f4c760", size = 73611154, upload-time = "2025-08-06T14:53:10.919Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c4/3e7a3887eba14e815e614db70b3b529112d1513d9dae6f4d43e373360b7f/torch-2.8.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:220a06fd7af8b653c35d359dfe1aaf32f65aa85befa342629f716acb134b9710", size = 102073391, upload-time = "2025-08-06T14:53:20.937Z" }, - { url = "https://files.pythonhosted.org/packages/5a/63/4fdc45a0304536e75a5e1b1bbfb1b56dd0e2743c48ee83ca729f7ce44162/torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c12fa219f51a933d5f80eeb3a7a5d0cbe9168c0a14bbb4055f1979431660879b", size = 888063640, upload-time = "2025-08-06T14:55:05.325Z" }, - { url = "https://files.pythonhosted.org/packages/84/57/2f64161769610cf6b1c5ed782bd8a780e18a3c9d48931319f2887fa9d0b1/torch-2.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c7ef765e27551b2fbfc0f41bcf270e1292d9bf79f8e0724848b1682be6e80aa", size = 241366752, upload-time = "2025-08-06T14:53:38.692Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5e/05a5c46085d9b97e928f3f037081d3d2b87fb4b4195030fc099aaec5effc/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:5ae0524688fb6707c57a530c2325e13bb0090b745ba7b4a2cd6a3ce262572916", size = 73621174, upload-time = "2025-08-06T14:53:25.44Z" }, - { url = "https://files.pythonhosted.org/packages/49/0c/2fd4df0d83a495bb5e54dca4474c4ec5f9c62db185421563deeb5dabf609/torch-2.8.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e2fab4153768d433f8ed9279c8133a114a034a61e77a3a104dcdf54388838705", size = 101906089, upload-time = "2025-08-06T14:53:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/99/a8/6acf48d48838fb8fe480597d98a0668c2beb02ee4755cc136de92a0a956f/torch-2.8.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2aca0939fb7e4d842561febbd4ffda67a8e958ff725c1c27e244e85e982173c", size = 887913624, upload-time = "2025-08-06T14:56:44.33Z" }, - { url = "https://files.pythonhosted.org/packages/af/8a/5c87f08e3abd825c7dfecef5a0f1d9aa5df5dd0e3fd1fa2f490a8e512402/torch-2.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:2f4ac52f0130275d7517b03a33d2493bab3693c83dcfadf4f81688ea82147d2e", size = 241326087, upload-time = "2025-08-06T14:53:46.503Z" }, - { url = "https://files.pythonhosted.org/packages/be/66/5c9a321b325aaecb92d4d1855421e3a055abd77903b7dab6575ca07796db/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:619c2869db3ada2c0105487ba21b5008defcc472d23f8b80ed91ac4a380283b0", size = 73630478, upload-time = "2025-08-06T14:53:57.144Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b0/a321f27270049baa12f5c3fb0d6ceea005634787e3af9a8d75dce8306b0a/torch-2.8.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:da6afa31c13b669d4ba49d8a2169f0db2c3ec6bec4af898aa714f401d4c38904", size = 102059214, upload-time = "2025-08-06T14:55:33.433Z" }, - { url = "https://files.pythonhosted.org/packages/fd/dd/1630cb51b10d3d2e97db95e5a84c32def81fc26b005bce6fc880b0e6db81/torch-2.8.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:06fcee8000e5c62a9f3e52a688b9c5abb7c6228d0e56e3452983416025c41381", size = 888024302, upload-time = "2025-08-06T14:57:28.23Z" }, - { url = "https://files.pythonhosted.org/packages/b9/dc/1f1f621afe15e3c496e1e8f94f8903f75f87e7d642d5a985e92210cc208d/torch-2.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:5128fe752a355d9308e56af1ad28b15266fe2da5948660fad44de9e3a9e36e8c", size = 241249338, upload-time = "2025-08-06T14:57:05.669Z" }, - { url = "https://files.pythonhosted.org/packages/ae/95/ae26263aceb3d57b821179f827d0e321373ed49423e603dd5906ab14a730/torch-2.8.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:e9f071f5b52a9f6970dc8a919694b27a91ae9dc08898b2b988abbef5eddfd1ae", size = 73610795, upload-time = "2025-08-06T14:57:11.513Z" }, -] - -[[package]] -name = "torch" -version = "2.10.0" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "cuda-bindings", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "fsspec", version = "2026.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "jinja2", marker = "python_full_version >= '3.10'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "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 = "nvidia-cublas-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", version = "2.27.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvshmem-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "setuptools", marker = "python_full_version >= '3.12'" }, - { name = "sympy", marker = "python_full_version >= '3.10'" }, - { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, -] -wheels = [ - { 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" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f2/c1690994afe461aae2d0cac62251e6802a703dec0a6c549c02ecd0de92a9/torch-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2c0d7fcfbc0c4e8bb5ebc3907cbc0c6a0da1b8f82b1fc6e14e914fa0b9baf74e", size = 80526521, upload-time = "2026-03-23T18:12:06.86Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f0/98ae802fa8c09d3149b0c8690741f3f5753c90e779bd28c9613257295945/torch-2.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4cf8687f4aec3900f748d553483ef40e0ac38411c3c48d0a86a438f6d7a99b18", size = 419723025, upload-time = "2026-03-23T18:11:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/18a9b10b4bd34f12d4e561c52b0ae7158707b8193c6cfc0aad2b48167090/torch-2.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1b32ceda909818a03b112006709b02be1877240c31750a8d9c6b7bf5f2d8a6e5", size = 530589207, upload-time = "2026-03-23T18:11:23.756Z" }, + { url = "https://files.pythonhosted.org/packages/35/40/2d532e8c0e23705be9d1debce5bc37b68d59a39bda7584c26fe9668076fe/torch-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3c712ae6fb8e7a949051a953fc412fe0a6940337336c3b6f905e905dac5157f", size = 114518313, upload-time = "2026-03-23T18:11:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0d/98b410492609e34a155fa8b121b55c7dca229f39636851c3a9ec20edea21/torch-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b6a60d48062809f58595509c524b88e6ddec3ebe25833d6462eeab81e5f2ce4", size = 80529712, upload-time = "2026-03-23T18:12:02.608Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/acea680005f098f79fd70c1d9d5ccc0cb4296ec2af539a0450108232fc0c/torch-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d91aac77f24082809d2c5a93f52a5f085032740a1ebc9252a7b052ef5a4fddc6", size = 419718178, upload-time = "2026-03-23T18:10:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8b/d7be22fbec9ffee6cff31a39f8750d4b3a65d349a286cf4aec74c2375662/torch-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7aa2f9bbc6d4595ba72138026b2074be1233186150e9292865e04b7a63b8c67a", size = 530604548, upload-time = "2026-03-23T18:10:03.569Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bd/9912d30b68845256aabbb4a40aeefeef3c3b20db5211ccda653544ada4b6/torch-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:73e24aaf8f36ab90d95cd1761208b2eb70841c2a9ca1a3f9061b39fc5331b708", size = 114519675, upload-time = "2026-03-23T18:11:52.995Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, + { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" }, ] [[package]] @@ -5351,8 +3601,7 @@ dependencies = [ { name = "lightning-utilities" }, { name = "numpy" }, { name = "packaging" }, - { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "torch" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/2e/48a887a59ecc4a10ce9e8b35b3e3c5cef29d902c4eac143378526e7485cb/torchmetrics-1.8.2.tar.gz", hash = "sha256:cf64a901036bf107f17a524009eea7781c9c5315d130713aeca5747a686fe7a5", size = 580679, upload-time = "2025-09-03T14:00:54.077Z" } wheels = [ @@ -5361,74 +3610,26 @@ wheels = [ [[package]] name = "torchvision" -version = "0.23.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.10'" }, - { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/49/5ad5c3ff4920be0adee9eb4339b4fb3b023a0fc55b9ed8dbc73df92946b8/torchvision-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7266871daca00ad46d1c073e55d972179d12a58fa5c9adec9a3db9bbed71284a", size = 1856885, upload-time = "2025-08-06T14:57:55.024Z" }, - { url = "https://files.pythonhosted.org/packages/25/44/ddd56d1637bac42a8c5da2c8c440d8a28c431f996dd9790f32dd9a96ca6e/torchvision-0.23.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:31c583ba27426a3a04eca8c05450524105c1564db41be6632f7536ef405a6de2", size = 2394251, upload-time = "2025-08-06T14:58:01.725Z" }, - { url = "https://files.pythonhosted.org/packages/93/f3/3cdf55bbf0f737304d997561c34ab0176222e0496b6743b0feab5995182c/torchvision-0.23.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3932bf67256f2d095ce90a9f826f6033694c818856f4bb26794cf2ce64253e53", size = 8627497, upload-time = "2025-08-06T14:58:09.317Z" }, - { url = "https://files.pythonhosted.org/packages/97/90/02afe57c3ef4284c5cf89d3b7ae203829b3a981f72b93a7dd2a3fd2c83c1/torchvision-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:83ee5bf827d61a8af14620c0a61d8608558638ac9c3bac8adb7b27138e2147d1", size = 1600760, upload-time = "2025-08-06T14:57:56.783Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d7/15d3d7bd8d0239211b21673d1bac7bc345a4ad904a8e25bb3fd8a9cf1fbc/torchvision-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49aa20e21f0c2bd458c71d7b449776cbd5f16693dd5807195a820612b8a229b7", size = 1856884, upload-time = "2025-08-06T14:58:00.237Z" }, - { url = "https://files.pythonhosted.org/packages/dd/14/7b44fe766b7d11e064c539d92a172fa9689a53b69029e24f2f1f51e7dc56/torchvision-0.23.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:01dc33ee24c79148aee7cdbcf34ae8a3c9da1674a591e781577b716d233b1fa6", size = 2395543, upload-time = "2025-08-06T14:58:04.373Z" }, - { url = "https://files.pythonhosted.org/packages/79/9c/fcb09aff941c8147d9e6aa6c8f67412a05622b0c750bcf796be4c85a58d4/torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35c27941831b653f5101edfe62c03d196c13f32139310519e8228f35eae0e96a", size = 8628388, upload-time = "2025-08-06T14:58:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/93/40/3415d890eb357b25a8e0a215d32365a88ecc75a283f75c4e919024b22d97/torchvision-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:09bfde260e7963a15b80c9e442faa9f021c7e7f877ac0a36ca6561b367185013", size = 1600741, upload-time = "2025-08-06T14:57:59.158Z" }, - { url = "https://files.pythonhosted.org/packages/df/1d/0ea0b34bde92a86d42620f29baa6dcbb5c2fc85990316df5cb8f7abb8ea2/torchvision-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e0e2c04a91403e8dd3af9756c6a024a1d9c0ed9c0d592a8314ded8f4fe30d440", size = 1856885, upload-time = "2025-08-06T14:58:06.503Z" }, - { url = "https://files.pythonhosted.org/packages/e2/00/2f6454decc0cd67158c7890364e446aad4b91797087a57a78e72e1a8f8bc/torchvision-0.23.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6dd7c4d329a0e03157803031bc856220c6155ef08c26d4f5bbac938acecf0948", size = 2396614, upload-time = "2025-08-06T14:58:03.116Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b5/3e580dcbc16f39a324f3dd71b90edbf02a42548ad44d2b4893cc92b1194b/torchvision-0.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4e7d31c43bc7cbecbb1a5652ac0106b436aa66e26437585fc2c4b2cf04d6014c", size = 8627108, upload-time = "2025-08-06T14:58:12.956Z" }, - { url = "https://files.pythonhosted.org/packages/82/c1/c2fe6d61e110a8d0de2f94276899a2324a8f1e6aee559eb6b4629ab27466/torchvision-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2e45272abe7b8bf0d06c405e78521b5757be1bd0ed7e5cd78120f7fdd4cbf35", size = 1600723, upload-time = "2025-08-06T14:57:57.986Z" }, - { url = "https://files.pythonhosted.org/packages/d5/3e/f1f3bb3dd452b98ec2eba4820d777440abceb3d3a428a6c8243006fe47e5/torchvision-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b190db205f90206c230fc2f91cbdfd5733334babc0e0d19bddb90a40b8cf26c2", size = 1856927, upload-time = "2025-08-06T14:58:18.919Z" }, - { url = "https://files.pythonhosted.org/packages/f4/e2/aafc6af854e792d212ff58e459f8d5d807568dc3f2b49ec41b677275e5a9/torchvision-0.23.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6c74cbc1cbee26dd4f35f989cd80dccc40411f258dee476b29871dee4b483af0", size = 2392870, upload-time = "2025-08-06T14:58:21.303Z" }, - { url = "https://files.pythonhosted.org/packages/5d/06/09b6a917b3759ef000428af0aa2597f983e20d9fbbcfeb826750f778fe6d/torchvision-0.23.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:a9e9d7552d34547b80843eaf64ab0737b19b2e8bec2514286b8cfd30861ca8b5", size = 8630400, upload-time = "2025-08-06T14:58:24.139Z" }, - { url = "https://files.pythonhosted.org/packages/08/07/ae46106efbf4bbc0090078aa4c406c38282cbe4e637bdb4b7f2e984140af/torchvision-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:dc7ce5accbbb8c9df9a79f8cef6a6df042f28e2250a6ae0d2ca70b06473fa03b", size = 1600751, upload-time = "2025-08-06T14:58:20.027Z" }, -] - -[[package]] -name = "torchvision" -version = "0.25.0" +version = "0.26.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.10'" }, - { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, ] 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/74/b4/cdfee31e0402ea035135462cb0ab496e974d56fab6b4e7a1f0cbccb8cd28/torchvision-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a06d4772a8e13e772906ed736cc53ec6639e5e60554f8e5fa6ca165aabebc464", size = 1863503, upload-time = "2026-03-23T18:13:01.384Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/11fee109841e80ad14e5ca2d80bff6b10eb11b7838ff06f35bfeaa9f7251/torchvision-0.26.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2adfbe438473236191ff077a4a9a0c767436879c89628aa97137e959b0c11a94", size = 7766423, upload-time = "2026-03-23T18:12:56.049Z" }, + { url = "https://files.pythonhosted.org/packages/5e/00/24d8c7845c3f270153fb81395a5135b2778e2538e81d14c6aea5106c689c/torchvision-0.26.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b6f9ad1ecc0eab52647298b379ee9426845f8903703e6127973f8f3d049a798b", size = 7518249, upload-time = "2026-03-23T18:12:51.743Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ed/e53cd7c0da7ae002e5e929c1796ebbe7ec0c700c29f7a0a6696497fb3d8b/torchvision-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:f13f12b3791a266de2d599cb8162925261622a037d87fc03132848343cf68f75", size = 3669784, upload-time = "2026-03-23T18:12:49.949Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bd/d552a2521bade3295b2c6e7a4a0d1022261cab7ca7011f4e2a330dbb3caa/torchvision-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55bd6ad4ae77be01ba67a410b05b51f53b0d0ee45f146eb6a0dfb9007e70ab3c", size = 1863499, upload-time = "2026-03-23T18:12:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/33/bf/21b899792b08cae7a298551c68398a79e333697479ed311b3b067aab4bdc/torchvision-0.26.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1c55dc8affbcc0eb2060fbabbe996ae9e5839b24bb6419777f17848945a411b1", size = 7767527, upload-time = "2026-03-23T18:12:44.348Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/57bbf9e216850d065e66dd31a50f57424b607f1d878ab8956e56a1f4e36b/torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd10b5f994c210f4f6d6761cf686f82d748554adf486cb0979770c3252868c8f", size = 7519925, upload-time = "2026-03-23T18:12:53.283Z" }, + { url = "https://files.pythonhosted.org/packages/10/58/ed8f7754299f3e91d6414b6dc09f62b3fa7c6e5d63dfe48d69ab81498a37/torchvision-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:de6424b12887ad884f39a0ee446994ae3cd3b6a00a9cafe1bead85a031132af0", size = 3983834, upload-time = "2026-03-23T18:13:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e7/56b47cc3b132aea90ccce22bcb8975dec688b002150012acc842846039d0/torchvision-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c409e1c3fdebec7a3834465086dbda8bf7680eff79abf7fd2f10c6b59520a7a4", size = 1863502, upload-time = "2026-03-23T18:12:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ec/5c31c92c08b65662fe9604a4067ae8232582805949f11ddc042cebe818ed/torchvision-0.26.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:406557718e62fdf10f5706e88d8a5ec000f872da913bf629aab9297622585547", size = 7767944, upload-time = "2026-03-23T18:12:42.805Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d8/cb6ccda1a1f35a6597645818641701207b3e8e13553e75fce5d86bac74b2/torchvision-0.26.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d61a5abb6b42a0c0c311996c2ac4b83a94418a97182c83b055a2a4ae985e05aa", size = 7522205, upload-time = "2026-03-23T18:12:54.654Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a9/c272623a0f735c35f0f6cd6dc74784d4f970e800cf063bb76687895a2ab9/torchvision-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:7993c01648e7c61d191b018e84d38fe0825c8fcb2720cd0f37caf7ba14404aa1", size = 4255155, upload-time = "2026-03-23T18:12:32.652Z" }, ] [[package]] @@ -5483,36 +3684,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] -[[package]] -name = "triton" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "importlib-metadata", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "setuptools", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/ee/0ee5f64a87eeda19bbad9bc54ae5ca5b98186ed00055281fd40fb4beb10e/triton-3.4.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ff2785de9bc02f500e085420273bb5cc9c9bb767584a4aa28d6e360cec70128", size = 155430069, upload-time = "2025-07-30T19:58:21.715Z" }, - { url = "https://files.pythonhosted.org/packages/7d/39/43325b3b651d50187e591eefa22e236b2981afcebaefd4f2fc0ea99df191/triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b70f5e6a41e52e48cfc087436c8a28c17ff98db369447bcaff3b887a3ab4467", size = 155531138, upload-time = "2025-07-30T19:58:29.908Z" }, - { url = "https://files.pythonhosted.org/packages/d0/66/b1eb52839f563623d185f0927eb3530ee4d5ffe9d377cdaf5346b306689e/triton-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c1d84a5c0ec2c0f8e8a072d7fd150cab84a9c239eaddc6706c081bfae4eb04", size = 155560068, upload-time = "2025-07-30T19:58:37.081Z" }, - { url = "https://files.pythonhosted.org/packages/12/34/1251beb5a3cb93f3950ebe68732752014646003ef6eb11eb5f1a37ca78cd/triton-3.4.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98e5c1442eaeabae2e2452ae765801bd53cd4ce873cab0d1bdd59a32ab2d9397", size = 155430799, upload-time = "2025-07-30T19:58:57.664Z" }, -] - [[package]] name = "triton" version = "3.6.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] wheels = [ + { url = "https://files.pythonhosted.org/packages/44/ba/b1b04f4b291a3205d95ebd24465de0e5bf010a2df27a4e58a9b5f039d8f2/triton-3.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781", size = 175972180, upload-time = "2026-01-20T16:15:53.664Z" }, { 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/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, { 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/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, { 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" }, ] @@ -5521,8 +3702,7 @@ name = "typer-slim" version = "0.21.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" } @@ -5554,8 +3734,7 @@ version = "2.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "black" }, - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "libcst" }, { name = "moreorless" }, { name = "tomlkit" }, @@ -5592,8 +3771,7 @@ version = "1.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "libcst" }, { name = "moreorless" }, { name = "stdlibs" }, @@ -5614,39 +3792,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/c1/d73f12f8cdb1891334a2ccf7389eed244d3941e74d80dd220badb937f3fb/wcwidth-0.5.3-py3-none-any.whl", hash = "sha256:d584eff31cd4753e1e5ff6c12e1edfdb324c995713f75d26c29807bb84bf649e", size = 92981, upload-time = "2026-01-31T03:52:09.14Z" }, ] -[[package]] -name = "webcolors" -version = "24.11.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/7b/29/061ec845fb58521848f3739e466efd8250b4b7b98c1b6c5bf4d40b419b7e/webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6", size = 45064, upload-time = "2024-11-11T07:43:24.224Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/e8/c0e05e4684d13459f93d312077a9a2efbe04d59c393bc2b8802248c908d4/webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9", size = 14934, upload-time = "2024-11-11T07:43:22.529Z" }, -] - [[package]] name = "webcolors" version = "25.10.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, @@ -5703,53 +3852,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/4f/fe9a4d472aa867878ce3bb7efb16654c5d63672b86dc0e6e953a67018433/yacs-0.1.8-py3-none-any.whl", hash = "sha256:99f893e30497a4b66842821bac316386f7bd5c4f47ad35c9073ef089aa33af32", size = 14747, upload-time = "2020-08-10T16:37:46.4Z" }, ] -[[package]] -name = "yt-dlp" -version = "2025.10.14" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/03/b7/dab729345e22891e79294273bc59c5213a1ec87331f49cb82ccea2b1bc9f/yt_dlp-2025.10.14.tar.gz", hash = "sha256:b18436aa9bb6f04354fd78d31ad9eeaae8c81b6a859f07072b25c18cd6c25844", size = 3045272, upload-time = "2025-10-14T23:39:52.688Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/19/399c85d29bd7b366b31ede82698f7963374e5a3842ae9de0cde6514506b0/yt_dlp-2025.10.14-py3-none-any.whl", hash = "sha256:0b9da17eda1bbf48e2315130043d7993fd4ca1c5a35571f8231da1a910c9c115", size = 3248664, upload-time = "2025-10-14T23:39:49.95Z" }, -] - [[package]] name = "yt-dlp" version = "2026.2.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version >= '3.12' and sys_platform == 'emscripten'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/16/be/8e099f3f34bac6851490525fb1a8b62d525a95fcb5af082e8c52ba884fb5/yt_dlp-2026.2.4.tar.gz", hash = "sha256:24733ef081116f29d8ee6eae7a48127101e6c56eb7aa228dd604a60654760022", size = 3100305, upload-time = "2026-02-04T00:49:27.043Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/96/38/b17cbeaf6712a4c1b97f7f9ec3a55f3a8ddee678cc88742af47dca0315b7/yt_dlp-2026.2.4-py3-none-any.whl", hash = "sha256:d6ea83257e8127a0097b1d37ee36201f99a292067e4616b2e5d51ab153b3dbb9", size = 3299165, upload-time = "2026-02-04T00:49:25.31Z" }, ] -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -] - [[package]] name = "zstandard" version = "0.25.0" @@ -5806,20 +3917,4 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/d0a405dad6ab6f9f759c26d866cca66cb209bff6f8db656074d662a953dd/zstandard-0.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b9af1fe743828123e12b41dd8091eca1074d0c1569cc42e6e1eee98027f2bbd0", size = 795263, upload-time = "2025-09-14T22:18:21.683Z" }, - { url = "https://files.pythonhosted.org/packages/ca/aa/ceb8d79cbad6dabd4cb1178ca853f6a4374d791c5e0241a0988173e2a341/zstandard-0.25.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b14abacf83dfb5c25eb4e4a79520de9e7e205f72c9ee7702f91233ae57d33a2", size = 640560, upload-time = "2025-09-14T22:18:22.867Z" }, - { url = "https://files.pythonhosted.org/packages/88/cd/2cf6d476131b509cc122d25d3416a2d0aa17687ddbada7599149f9da620e/zstandard-0.25.0-cp39-cp39-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:a51ff14f8017338e2f2e5dab738ce1ec3b5a851f23b18c1ae1359b1eecbee6df", size = 5344244, upload-time = "2025-09-14T22:18:24.724Z" }, - { url = "https://files.pythonhosted.org/packages/5c/71/e14820b61a1c137966b7667b400b72fa4a45c836257e443f3d77607db268/zstandard-0.25.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3b870ce5a02d4b22286cf4944c628e0f0881b11b3f14667c1d62185a99e04f53", size = 5054550, upload-time = "2025-09-14T22:18:26.445Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ce/26dc5a6fa956be41d0e984909224ed196ee6f91d607f0b3fd84577741a77/zstandard-0.25.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:05353cef599a7b0b98baca9b068dd36810c3ef0f42bf282583f438caf6ddcee3", size = 5401150, upload-time = "2025-09-14T22:18:28.745Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1b/402cab5edcfe867465daf869d5ac2a94930931c0989633bc01d6a7d8bd68/zstandard-0.25.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19796b39075201d51d5f5f790bf849221e58b48a39a5fc74837675d8bafc7362", size = 5448595, upload-time = "2025-09-14T22:18:30.475Z" }, - { url = "https://files.pythonhosted.org/packages/86/b2/fc50c58271a1ead0e5a0a0e6311f4b221f35954dce438ce62751b3af9b68/zstandard-0.25.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53e08b2445a6bc241261fea89d065536f00a581f02535f8122eba42db9375530", size = 5555290, upload-time = "2025-09-14T22:18:32.336Z" }, - { url = "https://files.pythonhosted.org/packages/d2/20/5f72d6ba970690df90fdd37195c5caa992e70cb6f203f74cc2bcc0b8cf30/zstandard-0.25.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1f3689581a72eaba9131b1d9bdbfe520ccd169999219b41000ede2fca5c1bfdb", size = 5043898, upload-time = "2025-09-14T22:18:34.215Z" }, - { url = "https://files.pythonhosted.org/packages/e4/f1/131a0382b8b8d11e84690574645f528f5c5b9343e06cefd77f5fd730cd2b/zstandard-0.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d8c56bb4e6c795fc77d74d8e8b80846e1fb8292fc0b5060cd8131d522974b751", size = 5571173, upload-time = "2025-09-14T22:18:36.117Z" }, - { url = "https://files.pythonhosted.org/packages/53/f6/2a37931023f737fd849c5c28def57442bbafadb626da60cf9ed58461fe24/zstandard-0.25.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:53f94448fe5b10ee75d246497168e5825135d54325458c4bfffbaafabcc0a577", size = 4958261, upload-time = "2025-09-14T22:18:38.098Z" }, - { url = "https://files.pythonhosted.org/packages/b5/52/ca76ed6dbfd8845a5563d3af4e972da3b9da8a9308ca6b56b0b929d93e23/zstandard-0.25.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c2ba942c94e0691467ab901fc51b6f2085ff48f2eea77b1a48240f011e8247c7", size = 5265680, upload-time = "2025-09-14T22:18:39.834Z" }, - { url = "https://files.pythonhosted.org/packages/7a/59/edd117dedb97a768578b49fb2f1156defb839d1aa5b06200a62be943667f/zstandard-0.25.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:07b527a69c1e1c8b5ab1ab14e2afe0675614a09182213f21a0717b62027b5936", size = 5439747, upload-time = "2025-09-14T22:18:41.647Z" }, - { url = "https://files.pythonhosted.org/packages/75/71/c2e9234643dcfbd6c5e975e9a2b0050e1b2afffda6c3a959e1b87997bc80/zstandard-0.25.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:51526324f1b23229001eb3735bc8c94f9c578b1bd9e867a0a646a3b17109f388", size = 5818805, upload-time = "2025-09-14T22:18:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/f5/93/8ebc19f0a31c44ea0e7348f9b0d4b326ed413b6575a3c6ff4ed50222abb6/zstandard-0.25.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89c4b48479a43f820b749df49cd7ba2dbc2b1b78560ecb5ab52985574fd40b27", size = 5362280, upload-time = "2025-09-14T22:18:45.625Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e9/29cc59d4a9d51b3fd8b477d858d0bd7ab627f700908bf1517f46ddd470ae/zstandard-0.25.0-cp39-cp39-win32.whl", hash = "sha256:1cd5da4d8e8ee0e88be976c294db744773459d51bb32f707a0f166e5ad5c8649", size = 436460, upload-time = "2025-09-14T22:18:49.077Z" }, - { url = "https://files.pythonhosted.org/packages/41/b5/bc7a92c116e2ef32dc8061c209d71e97ff6df37487d7d39adb51a343ee89/zstandard-0.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:37daddd452c0ffb65da00620afb8e17abd4adaae6ce6310702841760c2c26860", size = 506097, upload-time = "2025-09-14T22:18:47.342Z" }, ]