From 0e6114ac36e23038fafbbcaed89c2917aeb00fc5 Mon Sep 17 00:00:00 2001 From: masato-ka Date: Sat, 9 May 2026 03:27:01 +0900 Subject: [PATCH 01/18] fix(train): restrict legacy RA-BC migration to JSON checkpoints only (#3490) * fix(train): restrict legacy RA-BC migration to JSON checkpoints only _migrate_legacy_rabc_fields was called for all config files, causing json.load to raise DecodeError when a YAML/TOML config was passed to lerobot-train for a new training run. Guard the block with an .endswith(".json") check so migration only runs when resuming from a JSON checkpoint. --- src/lerobot/configs/train.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lerobot/configs/train.py b/src/lerobot/configs/train.py index b2b3cd7a0..318821166 100644 --- a/src/lerobot/configs/train.py +++ b/src/lerobot/configs/train.py @@ -256,7 +256,9 @@ class TrainPipelineConfig(HubMixin): ) from e cli_args = kwargs.pop("cli_args", []) - if config_file is not None: + # Legacy RA-BC migration only applies to framework-saved checkpoints (always JSON). + # Hand-written YAML/TOML configs are expected to use the current sample_weighting schema. + if config_file is not None and config_file.endswith(".json"): with open(config_file) as f: config = json.load(f) migrated_config = _migrate_legacy_rabc_fields(config) From 5699f6cbf4ad1b1a5da162df03401692fc66f2f5 Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Sun, 10 May 2026 11:49:31 +0200 Subject: [PATCH 02/18] chore(ci): disable auto-stale (#3550) --- .github/workflows/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e55410cdf..b511b6171 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -19,8 +19,8 @@ on: workflow_dispatch: # Runs at 02:00 - schedule: - - cron: "0 2 * * *" + # schedule: + # - cron: "0 2 * * *" env: CLOSE_ISSUE_MESSAGE: > From cb8edf17e6671069f8bcdc167418cf394c0ea2ac Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Sun, 10 May 2026 12:24:22 +0200 Subject: [PATCH 03/18] chore(dependencies): update uv.lock (#3475) --- uv.lock | 836 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 442 insertions(+), 394 deletions(-) diff --git a/uv.lock b/uv.lock index 951163f55..bd5036a9e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.15' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", @@ -276,6 +276,44 @@ 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 = "ast-serialize" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/9d/912fefab0e30aee6a3af8a62bbea4a81b29afa4ba2c973d31170620a26de/ast_serialize-0.3.0.tar.gz", hash = "sha256:1bc3ca09a63a021376527c4e938deedd11d11d675ce850e6f9c7487f5889992b", size = 60689, upload-time = "2026-04-30T23:24:48.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/57/a54d4de491d6cdd7a4e4b0952cc3ca9f60dcefa7b5fb48d6d492debe1649/ast_serialize-0.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a867927df59f76a18dc1d874a0b2c079b42c58972dca637905576deb0912e14", size = 1182966, upload-time = "2026-04-30T23:23:57.376Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/a5db014bb0f91b209236b57c429389e31290c0093532b8436d577699b2fa/ast_serialize-0.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a6fb063bf040abf8321e7b8113a0554eda445ffc508aa51287f8808886a5ae22", size = 1171316, upload-time = "2026-04-30T23:23:59.63Z" }, + { url = "https://files.pythonhosted.org/packages/15/59/fd55133e478c4326f60a11df02573bf7ccb2ac685810b50f1803d0f68053/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5075cd8482573d743586779e5f9b652a015e37d4e95132d7e5a9bc5c8f483d8f", size = 1232234, upload-time = "2026-04-30T23:24:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/cc/79/0ca1d26357ecb4a697d74d00b73ef3137f24c140424125393a0de820eb09/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41560b27794f4553b0f77811e9fb325b77db4a2b39018d437e09932275306e66", size = 1233437, upload-time = "2026-04-30T23:24:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/7078ec94dd6e124b8e028ac77016a4f13c83fa1c145790f2e68f3816998b/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b967c01ca74909c5d90e0fe4393401e2cc5da5ebd9a6262a19e45ffd3757dec8", size = 1440188, upload-time = "2026-04-30T23:24:04.717Z" }, + { url = "https://files.pythonhosted.org/packages/21/16/cca7195ef55a012f8013c3442afa91d287a0a36dcf88b480b262475135b3/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:424ebb8f46cd993f7cec4009d119312d8433dd90e6b0df0499cd2c91bdcc5af9", size = 1254211, upload-time = "2026-04-30T23:24:06.18Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/f3d4dfae67dee6580534361a6343367d34217e7d25cff858bd1d8f03b8ed/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14b1d566b56e2ee70b11fec1de7e0b94ec7cd83717ec7d189967841a361190e", size = 1255973, upload-time = "2026-04-30T23:24:07.772Z" }, + { url = "https://files.pythonhosted.org/packages/14/41/55fbfe02c42f40fbe3e74eda167d977d555ff720ce1abfa08515236efd88/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ba30b18735f047ec11103d1ab92f4789cf1fea1e0dc89b04a2f5a0632fd79de", size = 1298629, upload-time = "2026-04-30T23:24:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/28/36/7d2501cacc7989fb8504aa9da2a2022a174200a59d4e6639de4367a57fdd/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ea0754cb7b0f682ebb005ffb0d18f8d17993490d9c289863cd69cacc4ab8df", size = 1408435, upload-time = "2026-04-30T23:24:11.013Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/54e3b469c3fa0bf9cd532fa643d1d33b73303f8d70beac3e366b68dd64b7/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a0c5aa1073a5ba7b2abaa4b54abe8b8d75c4d1e2d54a2ff70b0ca6222fea5728", size = 1508174, upload-time = "2026-04-30T23:24:12.635Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/9b9621865b02c60539e26d9b114a312b4fa46aa703e33e79317174bfea21/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4e52650d834c1ea7791969a361de2c54c13b2fb4c519ec79445fa8b9021a147d", size = 1502354, upload-time = "2026-04-30T23:24:14.186Z" }, + { url = "https://files.pythonhosted.org/packages/34/dd/f138bc5c43b0c414fdd12eefe15677839323078b6e75301ad7f96cd26d45/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15bd6af3f136c61dae27805eb6b8f3269e85a545c4c27ffe9e530ead78d2b36d", size = 1450504, upload-time = "2026-04-30T23:24:16.076Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/97ef9e1c315601db74365955c8edd3292e3055500d6317602815dbdf08ae/ast_serialize-0.3.0-cp314-cp314t-win32.whl", hash = "sha256:d188bfe37b674b49708497683051d4b571366a668799c9b8e8a94513694969d9", size = 1058662, upload-time = "2026-04-30T23:24:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d6/e2c3483c31580fdb623f92ad38d2f856cde4b9205a3e6bd84760f3de7d82/ast_serialize-0.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5832c2fdf8f8a6cf682b4cfcf677f5eaf39b4ddbc490f5480cfccdd1e7ce8fa1", size = 1100349, upload-time = "2026-04-30T23:24:18.992Z" }, + { url = "https://files.pythonhosted.org/packages/ab/89/29abcb1fe18a429cda60c6e0bbd1d6e90499339842a2f548d7567542357e/ast_serialize-0.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:670f177188d128fb7f9f15b5ad0e1b553d22c34e3f584dcb83eb8077600437f0", size = 1072895, upload-time = "2026-04-30T23:24:20.706Z" }, + { url = "https://files.pythonhosted.org/packages/bc/93/72abad83966ed6235647c9f956417dc1e17e997696388521910e3d1fa3f4/ast_serialize-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ec2fafa5e4313cc8feed96e436ebe19ac7bc6fa41fbc2827e826c48b9e4c3a9", size = 1190024, upload-time = "2026-04-30T23:24:22.486Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/eb88584b2f0234e581762011208ca203252bf6c98e59b4769daa571f3576/ast_serialize-0.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef6d3c08b7b4cd29b48410338e134764a00e76d25841eb02c1084e868c888ecc", size = 1178633, upload-time = "2026-04-30T23:24:24.35Z" }, + { url = "https://files.pythonhosted.org/packages/56/51/cf1ec1ff3e616373d0dcbd5fad502e0029dc541f13ab642259762a7d127f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d841424f41b886e98044abc80769c14a956e6e5ccd5fb5b0d9f5ead72be18a4", size = 1241351, upload-time = "2026-04-30T23:24:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/68fcf50478cf1093f2d423f034ae06453122c8b415d8e21a44668eca485d/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d21453734ad39367ede5d37efe4f59f830ce1c09f432fc72a90e368f77a4a3e7", size = 1239582, upload-time = "2026-04-30T23:24:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c1/a6c9fa284eceb5fc6f21347e968445a051d7ca2c4d34e6a04314646dbcee/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5e110cdce2a347e1dd987529c88ef54d26f67848dce3eba1b3b2cc2cf085c94", size = 1448853, upload-time = "2026-04-30T23:24:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/23/5f/8ad3829a09e4e8c5328a53ce7d4711d660944e3e164c5f6abcc2c8f27167/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6e23a98e57560a055f5c4b68700a0fd5ce483d2814c23140b3638c7f5d1e61", size = 1262204, upload-time = "2026-04-30T23:24:31.482Z" }, + { url = "https://files.pythonhosted.org/packages/25/13/44aa28d97f10e25247e8576b5f6b2795d4fa1a80acc88acc942c508d06f7/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c9e763d70293d65ce1e1ea8c943140c68d0953f0268c7ee0998f2e07f77dd0", size = 1266458, upload-time = "2026-04-30T23:24:33.088Z" }, + { url = "https://files.pythonhosted.org/packages/d8/58/b3a8be3777cd3744324fd5cec0d80d37cd96fc7cbb0fb010e03dff1e870f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4388a1796c228f1ce5c391426f7d21a0003ad3b47f677dbeded9bd1a85c7209f", size = 1308700, upload-time = "2026-04-30T23:24:34.657Z" }, + { url = "https://files.pythonhosted.org/packages/13/03/f8312d6b57f5471a9dc7946f22b8798a1fc296d38c25766223aacadec42c/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5283cdcc0c64c3d8b9b688dc6aaa012d9c0cf1380a7f774a6bae6a1c01b3205a", size = 1416724, upload-time = "2026-04-30T23:24:36.562Z" }, + { url = "https://files.pythonhosted.org/packages/50/5d/13fc3789a7abac00559da2e2e9f386db4612aa1f84fc53d09bf714c37545/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ef88cc5842a5d7a6ac09dc0d5fc2c98f5d276c1f076f866d55047ce886785b", size = 1515441, upload-time = "2026-04-30T23:24:38.018Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b9/7ab43fc7a23b1f970281093228f5f79bed6edeed7a3e672bde6d7a832a58/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cc14bf402bdc0978594ecce783793de2c7470cd4f5cd7eb286ca97ed8ff7cba9", size = 1510522, upload-time = "2026-04-30T23:24:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/56/ec/d75fc2b788d319f1fad77c14156896f31afdfc68af85b505e5bdebcb9592/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11eae0cf1b7b3e0678133cc2daa974ea972caf02eb4b3aa062af6fa9acd52c57", size = 1460917, upload-time = "2026-04-30T23:24:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/95/74/f99c81193a2725911e1911ae567ed27c2f2419332c7f3537366f9d238cac/ast_serialize-0.3.0-cp39-abi3-win32.whl", hash = "sha256:2db3dd99de5e6a5a11d7dda73de8750eb6e5baaf25245adf7bdcfe64b6108ae2", size = 1067804, upload-time = "2026-04-30T23:24:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/16/81/76af00c47daa151e89f98ae21fbbcb2840aaa9f5766579c4da76a3c57188/ast_serialize-0.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:a2cd125adccf7969470621905d302750cd25951f22ea430d9a25b7be031e5549", size = 1105561, upload-time = "2026-04-30T23:24:44.578Z" }, + { url = "https://files.pythonhosted.org/packages/bd/46/d3ec57ad500f598d1554bd14ce4df615960549ab2844961bc4e1f5fbd174/ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a", size = 1077165, upload-time = "2026-04-30T23:24:46.377Z" }, +] + [[package]] name = "asttokens" version = "3.0.1" @@ -587,11 +625,11 @@ wheels = [ [[package]] name = "cmeel" -version = "0.59.0" +version = "0.60.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/58/2448af92b3761a1b321014a653f79d322026681728f96ebe9f419ae0d6b8/cmeel-0.59.0.tar.gz", hash = "sha256:d9871f96ad0499c1cf8671e69622c805265a6be4383a1abfd18f20b4a33e3e3a", size = 14890, upload-time = "2026-01-19T11:48:25.431Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2b/a178a123602cb23b737289ae24fe9213bf1002660bb89d48e5dda62b46cc/cmeel-0.60.0.tar.gz", hash = "sha256:2e6d9ae61cc94112a67814b14948dd679b353090be4b87ab04c3ccaea3aa95de", size = 14935, upload-time = "2026-05-09T16:03:35.536Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/c7/f7a2ea2e88cba4828c9b5bba5b8448ad6e6cbd652d782cc97bb14a54e6a6/cmeel-0.59.0-py3-none-any.whl", hash = "sha256:04a24b960e602484306721ce148610ddda4cbc83b8c5f27ef915366a86901e06", size = 20991, upload-time = "2026-01-19T11:48:24.259Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/ffd1484c68ca7489596806f830446219540dd17508818fe0d2c2fb0f4f59/cmeel-0.60.0-py3-none-any.whl", hash = "sha256:ed0672f7cebbb1143e6e29fcc0d3fd26e100ed2381b49dd15444bd1dd6d3ce0b", size = 20573, upload-time = "2026-05-09T16:03:33.827Z" }, ] [[package]] @@ -1563,14 +1601,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.49" +version = "3.1.50" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/63/210aaa302d6a0a78daa67c5c15bbac2cad361722841278b0209b6da20855/gitpython-3.1.49.tar.gz", hash = "sha256:42f9399c9eb33fc581014bedd76049dfbaf6375aa2a5754575966387280315e1", size = 219367, upload-time = "2026-04-29T00:31:20.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl", hash = "sha256:024b0422d7f84d15cd794844e029ffebd4c5d42a7eb9b936b458697ef550a02c", size = 212190, upload-time = "2026-04-29T00:31:18.412Z" }, + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, ] [[package]] @@ -1810,34 +1848,34 @@ sdist = { url = "https://files.pythonhosted.org/packages/7e/ca/7f1c90aedcd067d60 [[package]] name = "hf-xet" -version = "1.4.3" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/43/724d307b34e353da0abd476e02f72f735cdd2bc86082dee1b32ea0bfee1d/hf_xet-1.4.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7551659ba4f1e1074e9623996f28c3873682530aee0a846b7f2f066239228144", size = 3800935, upload-time = "2026-03-31T22:39:49.618Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942, upload-time = "2026-03-31T22:39:47.938Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657, upload-time = "2026-03-31T22:39:39.69Z" }, - { url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765, upload-time = "2026-03-31T22:39:37.936Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162, upload-time = "2026-03-31T22:39:58.382Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525, upload-time = "2026-03-31T22:40:00.225Z" }, - { url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610, upload-time = "2026-03-31T22:40:10.42Z" }, - { url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529, upload-time = "2026-03-31T22:40:09.106Z" }, - { url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019, upload-time = "2026-03-31T22:39:56.651Z" }, - { url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746, upload-time = "2026-03-31T22:39:54.766Z" }, - { url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692, upload-time = "2026-03-31T22:39:46.246Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281, upload-time = "2026-03-31T22:39:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414, upload-time = "2026-03-31T22:40:04.951Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368, upload-time = "2026-03-31T22:40:06.347Z" }, - { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload-time = "2026-03-31T22:40:16.401Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload-time = "2026-03-31T22:40:14.995Z" }, - { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, - { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, - { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, - { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, ] [[package]] @@ -1930,7 +1968,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.13.0" +version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -1943,9 +1981,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/ff/ec7ed2eb43bd7ce8bb2233d109cc235c3e807ffe5e469dc09db261fac05e/huggingface_hub-1.13.0.tar.gz", hash = "sha256:f6df2dac5abe82ce2fe05873d10d5ff47bc677d616a2f521f4ee26db9415d9d0", size = 781788, upload-time = "2026-04-30T11:57:33.858Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/40/43109e943fd718b0ccd0cd61eb4f1c347df22bf81f5874c6f22adf44bcff/huggingface_hub-1.14.0.tar.gz", hash = "sha256:d6d2c9cd6be1d02ae9ec6672d5587d10a427f377db688e82528f426a041622c2", size = 782365, upload-time = "2026-05-06T14:14:34.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/db/4b1cdae9460ae1f3ca020cd767f013430ce23eb1d9c890ae3a0609b38d26/huggingface_hub-1.13.0-py3-none-any.whl", hash = "sha256:e942cb50d6a08dd5306688b1ac05bda157fd2fcc88b63dae405f7bd0d3234005", size = 660643, upload-time = "2026-04-30T11:57:31.802Z" }, + { url = "https://files.pythonhosted.org/packages/89/a5/33b49ba7bea7c41bb37f74ec0f8beea0831e052330196633fe2c77516ea6/huggingface_hub-1.14.0-py3-none-any.whl", hash = "sha256:efe075535c62e130b30e836b138e13785f6f043d1f0539e0a39aa411a99e90b8", size = 661479, upload-time = "2026-05-06T14:14:32.029Z" }, ] [[package]] @@ -2321,7 +2359,7 @@ wheels = [ [[package]] name = "jupyter-server" -version = "2.18.1" +version = "2.18.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2343,9 +2381,9 @@ dependencies = [ { name = "traitlets" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/b0/666586d557a71a58cd9960b154fb9aee0ed81dd62a50371195ab95731909/jupyter_server-2.18.1.tar.gz", hash = "sha256:f62be526369b791625e03bd658070563c1a4e9a0a2f439ea1f9dbacea5f7191a", size = 752024, upload-time = "2026-05-05T09:17:51.101Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/15/1eacb0fcb79ef86e8a0a79a708e6ad7435f6f223097dd29a4ce861fabc44/jupyter_server-2.18.2.tar.gz", hash = "sha256:06b4f40d8a7a00bb39d5216859c81374a0e7cfefe6d8a5a7facc5a5c37c679a7", size = 753177, upload-time = "2026-05-06T07:04:36.274Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/45/bfe3779fd06714a379128f2c4eaf7c99414f0eb081f9f34c135f6b3d511c/jupyter_server-2.18.1-py3-none-any.whl", hash = "sha256:db0374d52a975f88a92a7f20de44e08ef5be9763ba7e99630baf16c46ac8dbf0", size = 391844, upload-time = "2026-05-05T09:17:48.521Z" }, + { url = "https://files.pythonhosted.org/packages/e2/50/ecf4f70d65bdb7519b28a33d1b2fee8a4b4ba1ae1a92f15d97e877c5de21/jupyter_server-2.18.2-py3-none-any.whl", hash = "sha256:fa5e46539ded65791838035a2b6001f13e54d5f64b8b3752eb1e91fdd641a5b8", size = 391907, upload-time = "2026-05-06T07:04:34.014Z" }, ] [[package]] @@ -3122,62 +3160,62 @@ provides-extras = ["dataset", "training", "hardware", "viz", "core-scripts", "ev [[package]] name = "librt" -version = "0.9.0" +version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/cb/c1945e506893b5b8577fb45a60c80e3ffe4a82092a04a6f29b0b951d9a24/librt-0.10.0.tar.gz", hash = "sha256:1aba1e8aa4e3307a7be68a74149545fde7451964dc0235a8bec5704a17bdda42", size = 191799, upload-time = "2026-05-05T16:31:23.535Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, - { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, - { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, - { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, - { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, - { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, - { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, - { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, - { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, - { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, - { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, - { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, - { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, - { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, - { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, - { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, - { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, - { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, - { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, - { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, - { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, - { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, - { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, - { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, - { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, - { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, - { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, - { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, - { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, + { url = "https://files.pythonhosted.org/packages/12/8e/cbb5b6f6e45e65c10a42449a69eaccc44d73e6a081ea752fbc5221c6dc1c/librt-0.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b4b58a44b407e91f633dafee008de9ddea6aa2a555ed94929c099260910bd0ba", size = 77327, upload-time = "2026-05-05T16:29:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3d/8233cbee8e99e6a8992f02bfc2dec8d787509566a511d1fde2574ee7473f/librt-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:950b79b11762531bdf45a9df909d2f9a2a8445c70c88665c01d14c8511a27dc5", size = 79971, upload-time = "2026-05-05T16:29:40.96Z" }, + { url = "https://files.pythonhosted.org/packages/87/6f/5264b298cef2b72fc97d2dde56c66181eda35204bf5dcd1ed0c3d0a0a782/librt-0.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4538453f51be197633b425912c150e25b0667252d3741c53e8368176d98d9d37", size = 246559, upload-time = "2026-05-05T16:29:42.701Z" }, + { url = "https://files.pythonhosted.org/packages/07/7b/19b1b859cc60d5f99276cc2b3144d91556c6d1b1e4ebb50359696bebf7a8/librt-0.10.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:70b955f091beac93e994a0b7ec616934f63b3ea5c3d6d7af847562f935aceca7", size = 235216, upload-time = "2026-05-05T16:29:44.193Z" }, + { url = "https://files.pythonhosted.org/packages/6e/56/a2f40717142a8af46289f57874ef914353d8faccd5e4f8e594ab1e16e8c7/librt-0.10.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:483e685e06b6163728ba6c85d74315176be7190f432ec2a41226e5e14355d5f0", size = 263108, upload-time = "2026-05-05T16:29:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/ca/15c625c3bdc0167c01e04ef8878317e9713f3bfa788438342f7a94c7b22c/librt-0.10.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ac53d946a009d1a38c44a60812708c9458fb2a239a5f630d8e625571386650f", size = 255280, upload-time = "2026-05-05T16:29:48.087Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c5/ba301d571d9e05844e2435b73aba30bee77bb75ce155c9affcfd2173dd03/librt-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc8771c9fcf0ea894ca41fdc2abd83572c2fbda221f232d86e718614e57ff513", size = 268829, upload-time = "2026-05-05T16:29:49.628Z" }, + { url = "https://files.pythonhosted.org/packages/8b/60/af70e135bc1f1fe15dd3894b1e4bbefc7ecdf911749a925a39eb86ceb2a1/librt-0.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:70805dbc5257892ac572f86290a61e3c8d90224ecce1a8b2d1f7ed51965417f4", size = 262051, upload-time = "2026-05-05T16:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/c2/c8236eb8b421bac5a172ba208f965abaa89805da2a3fa112bdf1764caf8f/librt-0.10.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d3b4f300f7bcba6e2ff73fb8bef1898479e9772bfa2682998c636391633ec826", size = 264347, upload-time = "2026-05-05T16:29:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/15b6d32bc25dacd4a60886a683d8128d6219910c122202b995a40dd4f8d2/librt-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:943bc943f92f4fb3408fae62485c6a3ad68ce4f2ee205643a39641525c19a276", size = 286482, upload-time = "2026-05-05T16:29:54.675Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8e/b1b959bacd323eb4360579db992513e1406d1c6ef7edb57b5511fd0666fd/librt-0.10.0-cp312-cp312-win32.whl", hash = "sha256:6065c1a758fba1010b41401013903d3d5d2750eab425ddedd584abac31d0630e", size = 62955, upload-time = "2026-05-05T16:29:56.39Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4c/d4cd6e4b9fc24098e63cc85537d1b6689682aee96809c38f08072067cc2b/librt-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:d788ecbe208ab352dab0e105cc06057bf9a2fc7e58cabb0d751ad9e30062b9e2", size = 71191, upload-time = "2026-05-05T16:29:57.682Z" }, + { url = "https://files.pythonhosted.org/packages/2b/19/8641da1f63d24b92354a492f893c022d6b3a0df44e70c8eff49364613983/librt-0.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:6003d1f295bdba02656dc81308208fc060d0a51d8c0d0a6db70f7f3c57b9ba0a", size = 61432, upload-time = "2026-05-05T16:29:58.971Z" }, + { url = "https://files.pythonhosted.org/packages/e5/29/681a75c82f4cc90d29e4b257a3299b79fe13fe927a04c57b8109d70b6957/librt-0.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0ede79d682e73f91c1b599a76d78b7464b9b5d213754cedb13372d9df36e596", size = 77299, upload-time = "2026-05-05T16:30:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/62/24/0c7ca445a55d04be79cac19819437fd094782347fa116f6681844fa6143e/librt-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0ba0b131fdb336c8b9c948e397f4a7e649d0f783b529f07b647bf4961df392e", size = 79930, upload-time = "2026-05-05T16:30:01.555Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/1e2b8f6443ef9e9a81e89486ca70e22f3684f93db003ce6eaefc3d0839b9/librt-0.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2728117da2afb96fb957768725ee43dc9a2d73b031e02da424b818a3cdd3a275", size = 246195, upload-time = "2026-05-05T16:30:03.261Z" }, + { url = "https://files.pythonhosted.org/packages/74/61/9dc9e03de0439ad84c1c240aac8b747f12c90cb797ea6042f7bdb8d3410f/librt-0.10.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:723ba80594c49cdf0584196fc430752262605dc9449902fc9bd3d9b79976cb77", size = 234951, upload-time = "2026-05-05T16:30:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/635223117d7590875bca441275065a3bf491203ad4208bd1cc3ffd90c5a1/librt-0.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7292edaaca294a61a978c53a3c7d6130d099b0dfbc8f0a65916cdc6b891b9852", size = 262768, upload-time = "2026-05-05T16:30:06.638Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/b04152d0cd8b6ca2b428a8bd3230343230c35ed304a932f35b5375f2f828/librt-0.10.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89fe9d539f2c10a1666633eeeac507ce95dd06d9ecc58de3c6390dba156a3d3a", size = 255075, upload-time = "2026-05-05T16:30:08.216Z" }, + { url = "https://files.pythonhosted.org/packages/35/1e/25bac4c7f2ca36f0e612cade186970683cf79153d96beccc3a11a9e19b97/librt-0.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4efa7b9587503fa5b67f40593302b9c8836d211d222ff9f7cafe67be5f8f0b10", size = 268559, upload-time = "2026-05-05T16:30:10.1Z" }, + { url = "https://files.pythonhosted.org/packages/18/54/4601faab35b6632a13200faa146ca62bfd111ffbe2568be430d65c89493a/librt-0.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:22dc982ef59df0136df36092ccbdbb570ced8aafb33e49585739b2f1de1c13b6", size = 261753, upload-time = "2026-05-05T16:30:11.912Z" }, + { url = "https://files.pythonhosted.org/packages/1b/cf/39f4023509e94fade8b074666fa3292db9cb6b34ea5dcbe7af53df9fca1d/librt-0.10.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6f2e5f3606253a84cea719c94a3bb1c54487b5d617d0254d46e0920d8a06be3f", size = 264055, upload-time = "2026-05-05T16:30:13.465Z" }, + { url = "https://files.pythonhosted.org/packages/8e/00/40247209fc46a8e308a91412d5206aedf8efb667ee89eb625820106a5c2f/librt-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40884bfaa1e29f6b6a9be255007d8f359bfc9e61d68bdef8ed3158bfcbc95df9", size = 286190, upload-time = "2026-05-05T16:30:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/5566beb94431a985abe1787af5ef86e087750172ff9d0bbf20f93e88132d/librt-0.10.0-cp313-cp313-win32.whl", hash = "sha256:3cd34cd8254eba756660bff6c2da91278248184301054fe3e4feb073bdd49b14", size = 62949, upload-time = "2026-05-05T16:30:16.503Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c2/3ea3301d6c8dff51d39dbe8ed75db3dc92896947d4afb5eeadf821c1e67f/librt-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:7baac5313e2d8dce1386f97777a8d03ab28f5fe1e780b3b9ac2ee7544551fedc", size = 71152, upload-time = "2026-05-05T16:30:17.766Z" }, + { url = "https://files.pythonhosted.org/packages/3c/de/5d49cb92cadcbc77d3abc27b93fd6030ed8437487dde2eae38cab5e6704d/librt-0.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:afc5b4406c8e2515698d922a5c7823a009312835ea58196671fff40e35cb8166", size = 61336, upload-time = "2026-05-05T16:30:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/7165e08108cc185a13a9c069f0685e6ef92e70e07fddf7edf5e7348c6316/librt-0.10.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f09588a30e6a22ec624090d72a3ab1a6d4d5485c3ed739603e76aa3c16efa688", size = 76794, upload-time = "2026-05-05T16:30:20.392Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ef/bf8613febf651b90c5222ee79dea5ae58d4cc2b544df69d3033424448934/librt-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:131ade118d12bd7a0adc4e655474a553f1b76cf78385868885944d21d51e45e0", size = 79662, upload-time = "2026-05-05T16:30:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/b6/67/9eddd165c1d8397bdf99b38bf12b5a55b3def5035b49eedb49f2775d1430/librt-0.10.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8b9ab28e40d011c373a189eae900c916e66d6fbecf7983e9e4883089ee085ef", size = 242390, upload-time = "2026-05-05T16:30:23.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/d1/d95da80334501866cd37004ab5d7483220d05862fab4b5405394f0264f0d/librt-0.10.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:67c39bb30da73bae1f293d1ed8bc2f8f6642649dd0928d3600aeff3041ac23d6", size = 232603, upload-time = "2026-05-05T16:30:25.198Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fa/e6d64d28718bc1be4e1736fcb037ca1c4dfca927e7167df75a7d5215665e/librt-0.10.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c3273c6b774614f093c8927c2bf1b077d0fefde988fe98f46a333734e5597ab", size = 259187, upload-time = "2026-05-05T16:30:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/72/3f/3fdb77e7f937dad59cfd76b720be7e7643400ec76b2da35befab8d66ba30/librt-0.10.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9dd7c1b86a4baa583ab5db977484b93a2c474e69e96ef3e9538387ea54229cb9", size = 251846, upload-time = "2026-05-05T16:30:28.56Z" }, + { url = "https://files.pythonhosted.org/packages/18/ca/f4d49133dd86a6f55d79eca30bf412fa722f511a9abe67f62f57aa64e66a/librt-0.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a77385c5a202e831149f7ad03be9e67cf80e957e52c614e83dcb822c95222eb8", size = 264936, upload-time = "2026-05-05T16:30:30.491Z" }, + { url = "https://files.pythonhosted.org/packages/de/66/a8df2fbadc1f6c1827a096d11c40175bd526133480bd3bc88ec64a03d257/librt-0.10.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c6a5eafa74b5655bad59886138ed68426f098a6beb8cb95a71f2cc3cd8bb33fe", size = 258699, upload-time = "2026-05-05T16:30:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/1e3c83613fe05451bb969e27b68a573d177f08d5f63533cc29fec0989658/librt-0.10.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1fc93d0439204c50ab4d1512611ce2c206f1b369b419f69c7c27c761561e3291", size = 259825, upload-time = "2026-05-05T16:30:35.077Z" }, + { url = "https://files.pythonhosted.org/packages/09/24/5e2f926ee9d3ef348d9339526d7062abb5c44d8419e3179528c01d78c102/librt-0.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:79e713c178bc7a744adfbee6b4619a288eecc0c914da2a9313a20255abe2f0cf", size = 282548, upload-time = "2026-05-05T16:30:36.639Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7d/3e89ed6ad0162561fa8bef9df3195e24263104c955713cd0237d3711fad2/librt-0.10.0-cp314-cp314-win32.whl", hash = "sha256:2eba9d955a68c41d9f326be3da42f163ec3518b7ab20f1c826224e7bed71e0bf", size = 58970, upload-time = "2026-05-05T16:30:38.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/579e731c94a7086a268bfa3e7a4945cd47836bebd3cbf3faeafd2e7eaef9/librt-0.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbfaf7f5145e9917f5d18bffa298eff6a19d74e7b8b11dabdca95785befe8dbf", size = 67260, upload-time = "2026-05-05T16:30:39.804Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f8/235822b7ae0b2334f12ee18bcf2476d07924077a5efeea57dbe927704be2/librt-0.10.0-cp314-cp314-win_arm64.whl", hash = "sha256:8d6d385d1969849a6b1397114df22714b6ded917bada98668e3e974dc663477e", size = 57156, upload-time = "2026-05-05T16:30:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e3/9b919cbf1e8eb770bf91bb7df28125e0f1daf4587169afefd95402636e9a/librt-0.10.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:6c3a82d3bd32631ef5c79922dfc028520c9ad840255979ab4d908271818039ee", size = 79150, upload-time = "2026-05-05T16:30:42.761Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f5/72a944aa3bc3498169a168087eff58ca48b58bf1b704e59d091fd30739f3/librt-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d64cc66005dc324c9bb1fa3fc2841f529002f6eb15966d55e46d430f56955a6a", size = 82304, upload-time = "2026-05-05T16:30:44.082Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e3/fcc290a33e295019759472dfa794d204e43504b276ac65eab7fd9da20ea3/librt-0.10.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb562cd28c88cd2c6a9a6c78f99dc39348d6b16c94adc25de0e574acf1176e9", size = 272556, upload-time = "2026-05-05T16:30:45.497Z" }, + { url = "https://files.pythonhosted.org/packages/fd/54/546975e4c997573885e7f040a05012f8838e06fb12b0c3c1fbb76254e9d7/librt-0.10.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b809aa2854d019c28773b03605df22adc675ee4f3f4402d673581313e8906119", size = 256941, upload-time = "2026-05-05T16:30:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f1d03401571b331653acddbd4e8cd955c06d945241dd08b25192fac0d04b/librt-0.10.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc15acabdd519bd4176fdadc2119e5e3093485d86f89138daf47e5b4cedb983a", size = 285855, upload-time = "2026-05-05T16:30:48.86Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/62cf80ff046c339faf56718b3a940244d4beb70f1c6407289b5830ec11e9/librt-0.10.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b1b2d835307d08ddadd94568e2369648ec9173bd3eea6d7f52a1abe717c81f98", size = 275321, upload-time = "2026-05-05T16:30:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ea/da5918d4070362e9a4d2ee9cd34f9dc84902daad8fd4275f8504a727ff4e/librt-0.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d261c6a2f93335a5167887fb0223e8b98ffce20ee3fde242e8e58a37ece6d0e5", size = 293993, upload-time = "2026-05-05T16:30:52.577Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8d/68b6086bed1fcdc314c640ea04e31e52d18052e08059fa595409d66a51a9/librt-0.10.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e2ffd44963f8e7f68995504d90f9881d64e94dc1d8e310039b9526108fc0c0f7", size = 284254, upload-time = "2026-05-05T16:30:55.086Z" }, + { url = "https://files.pythonhosted.org/packages/06/c8/b810f1d84ec34a5a7ed93d7b510ab04164d75fbdf23088d5c3fbe6b08357/librt-0.10.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f285f6455ed495791c4d8630e5af732960adea93cac4c893d15619f2eae53e8", size = 284925, upload-time = "2026-05-05T16:30:56.728Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/3c82d4158c5a2c62528b8fccce65a8c9ad700e480e86f9389387435089a5/librt-0.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6034ff52e663d34c7b82ef2aa2f94ad7c1d939e2368e63b06844bc4d127d2e1", size = 307830, upload-time = "2026-05-05T16:30:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/99/3a/9c635ac3e8a00383ff689161d3eac8a30b3b2ddc711b40471e6b8983ea29/librt-0.10.0-cp314-cp314t-win32.whl", hash = "sha256:657860fd877fba6a241ea088ef99f63ca819945d3c715265da670bad56c37ebe", size = 60147, upload-time = "2026-05-05T16:31:00.293Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e8/6f65f3e565d4ac212cddddd552eacc8035ffdf941ca0ad6fe945a211d41f/librt-0.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56ded2d66010203a0cb5af063b609e3f079531a0e5e576d618dece859fd2e1af", size = 68649, upload-time = "2026-05-05T16:31:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/51/78/a0705a67cacd81e5fa01a5035b3adbdfbb43a7b8d4bd27e2b282ae61baf2/librt-0.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1ee63f30abf18ed4830fdbaf87b2b6f4bba1e198d46085c314edde4045e56715", size = 58247, upload-time = "2026-05-05T16:31:03.191Z" }, ] [[package]] @@ -3287,14 +3325,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -3416,26 +3454,26 @@ wheels = [ [[package]] name = "matplotlib-inline" -version = "0.2.1" +version = "0.2.2" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] [[package]] name = "mdit-py-plugins" -version = "0.5.0" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/3d/e0e8d9d1cee04f758120915e2b2a3a07eb41f8cf4654b4734788a522bcd1/mdit_py_plugins-0.6.0.tar.gz", hash = "sha256:2436f14a7295837ac9228a36feeabda867c4abc488c8d019ad5c0bda88eee040", size = 56025, upload-time = "2026-05-07T12:20:42.295Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, + { url = "https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl", hash = "sha256:f7e7a25d8b616fee99cb1e330da73451d11a8061baf39bb9663ab9ce0e005b90", size = 66655, upload-time = "2026-05-07T12:20:41.226Z" }, ] [[package]] @@ -3701,45 +3739,46 @@ wheels = [ [[package]] name = "mypy" -version = "1.20.2" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "ast-serialize" }, { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/dc/7e6d49f04fca40b9dd5c752a51a432ffe67fb45200702bc9eee0cb4bbb26/mypy-2.0.0.tar.gz", hash = "sha256:1a9e3900ac5c40f1fe813506c7739da6e6f0eab2729067ebd94bfb0bbba53532", size = 3869036, upload-time = "2026-05-06T19:26:43.22Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, - { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, - { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, - { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, - { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, - { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, - { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, - { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, - { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" }, - { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" }, - { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" }, - { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" }, - { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" }, - { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" }, - { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" }, - { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" }, - { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" }, - { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" }, - { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" }, - { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4b/f6cd12ef1eb63be1c342da3e8ca811d2280276177f6de4ef20cb2366d79b/mypy-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:660790551c988e69d8bf7a35c8b4149edeb22f4a339165702be843532e9dcdb5", size = 14756610, upload-time = "2026-05-06T19:26:19.221Z" }, + { url = "https://files.pythonhosted.org/packages/32/73/67d09ca28bee21feaca264b2a680cf2d300bcc2071136ad064928324c843/mypy-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7a15bf92cd8781f8e72f69ffa7e30d1f434402d065ee1ecd5223ef2ef100f914", size = 13554270, upload-time = "2026-05-06T19:26:08.977Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/44718b5c6b1b5a27440ff2effe6a1be0fa2a190c0f4e2e21a83728416f95/mypy-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ff370b43d7def05bbcd2f5267f0bcda72dd6a552ef2ea9375b02d6fe06da270", size = 13924663, upload-time = "2026-05-06T19:21:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2b/bbb9cc5773f946846a7c340097e59bcf84095437dda0d56bb4f6cf1f6541/mypy-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37bd246590a018e5a11703b7b09c39d47ede3df5ba3fa863c5b8590b465beb01", size = 14946862, upload-time = "2026-05-06T19:24:23.023Z" }, + { url = "https://files.pythonhosted.org/packages/43/25/e9318566f443a5130b4ff0ad3367ee6c4c4c49ff083fe5214a7318c18282/mypy-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cce87e92214fac8bf8feb8a680d0c1b6fb748d50e9b57fbb13e4b1d83a3ed19b", size = 15175090, upload-time = "2026-05-06T19:26:28.794Z" }, + { url = "https://files.pythonhosted.org/packages/67/65/2ec28c834f21e164c33bc296a7db538ad50c74f83e517c7a0be95ff6de86/mypy-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e19e9cb69b66a4141009d24898259914fa2b71d026de0b46edf9fafdbf4fd46e", size = 11052899, upload-time = "2026-05-06T19:25:39.084Z" }, + { url = "https://files.pythonhosted.org/packages/9e/72/d1ec625cfc9bd101c07a6834ef1f94e820296f8fdbad2eb03f50e0983f8c/mypy-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:b021614cb08d44785b025982163ec3c39c94bff766ead071fa9e82b4ef6f62cd", size = 9972935, upload-time = "2026-05-06T19:23:24.204Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c6/996a1e535e5d0d597c3b1460fc962733091f885f312e749350eb2ac10965/mypy-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ef5f581b61240d1cc629b12f8df6565ed6ffac0d82ed745eef7833222ab50b9", size = 14737259, upload-time = "2026-05-06T19:20:23.081Z" }, + { url = "https://files.pythonhosted.org/packages/94/c5/0f9460e26b77f434bd53f47d1ce32a3cd4580c92a5331fa5dfc059f9421a/mypy-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20e3470a165dbc249bdfbe8d1c5172727ef22688cffc279f8c3aa264ab9d4d9a", size = 13538377, upload-time = "2026-05-06T19:21:08.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/3e/8ea2f8dd1e5c9c279fb3c28193bdb850adf4d3d8172880abad829eced609/mypy-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:224ba142eee8b4d65d4db657cb1fc22abec30b135ded6ab297302ba1f62e505d", size = 13914264, upload-time = "2026-05-06T19:24:12.875Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/78bd3b8520f676acee9dab48ea71473e68f6d5cf14b59fbd800bea50a92b/mypy-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e879ad8a03908ff74d15e8a9b42bf049918e6798d52c011011f1873d0b5877e", size = 14926761, upload-time = "2026-05-06T19:20:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/61/ef/b52fa340522da3d22e669117c3b83155c2660f7cdc035856958fbfffb224/mypy-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65c5c15bcbd18d6fe927cc55c459597a3517d69cc3123f067be3b020010e115e", size = 15157014, upload-time = "2026-05-06T19:25:49.78Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/dde7614250c6d017936c7aa3bb63b9b52c7cfd298d3f1be9be45f307870b/mypy-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:d1a068acd7c9fb77e9f8923f1556f2f49d6d7895821121b8d97fa5642b9c52f5", size = 11067049, upload-time = "2026-05-06T19:21:16.116Z" }, + { url = "https://files.pythonhosted.org/packages/27/ec/1d6af4830a94a285442db19caa02f160cc1a255e4f324eec5458e6c2bafb/mypy-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:ef9d96da1ddffbc21f27d3939319b6846d12393baa17c4d2f3e81e040e73ce2c", size = 9967903, upload-time = "2026-05-06T19:22:15.52Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/6fefe954207860aed6eeb91776795e64a257d3ce0360862288984ce121f5/mypy-2.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c918c64e8ce36557851b0347f84eb12f1965d3a06813c36df253eb0c0afd1d82", size = 14729633, upload-time = "2026-05-06T19:24:53.383Z" }, + { url = "https://files.pythonhosted.org/packages/23/d6/d336f5b820af189eb0390cce21de62d264c0a4e64713dfbe81bfc4fc7739/mypy-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:301f1a8ccc7d79b542ee218b28bb49443a83e194eb3d10da63ff1649e5aa5d34", size = 13559524, upload-time = "2026-05-06T19:22:24.906Z" }, + { url = "https://files.pythonhosted.org/packages/af/a6/d7bb54fde1770f0484e5fbdbdce37a41e95ed0a1cd493ec60ead111e356c/mypy-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdf4ef489d44ce350bac3fd699907834e551d4c934e9cc862ef201215ab1558d", size = 13936018, upload-time = "2026-05-06T19:25:02.992Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ba/5be51316b91e6a6bf6e3a8adb3de500e7e1fb5bf9491743b8cbc81a34a2c/mypy-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cde2d0989f912fc850890f727d0d76495e7a6c5bdd9912a1efdb64952b4398d", size = 14910712, upload-time = "2026-05-06T19:25:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/e2c8c3b373e20ebfb66e6c83a99027fd67df4ec43b08879f74e822d2dc4c/mypy-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdf05693c231a14fe37dbfce192a3a1372c26a833af4a80f550547742952e719", size = 15141499, upload-time = "2026-05-06T19:20:50.924Z" }, + { url = "https://files.pythonhosted.org/packages/12/36/07756f933e00416d912e35878cfcf89a593a3350a885691c0bb85ae0226a/mypy-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:73aee2da33a2237e66cbe84a94780e53599847e86bb3aa7b93e405e8cd9905f2", size = 11240511, upload-time = "2026-05-06T19:21:32.39Z" }, + { url = "https://files.pythonhosted.org/packages/70/05/79ac1f20f2397353f3845f7b8bb5d8006cda7c8ef9092f04f9de3c6135f2/mypy-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:1f6dcd8f39971f41edab2728c877c4ac8b50ad3c387ff2770423b79a05d23910", size = 10149336, upload-time = "2026-05-06T19:22:08.383Z" }, + { url = "https://files.pythonhosted.org/packages/53/e0/0db84e0ebbad6e99e566c68e4b465784f2a2294f7719e8db9d509ef23087/mypy-2.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a04e980b9275c76159da66c6e1723c7798306f9802b31bdaf9358d0c84030ce8", size = 15797362, upload-time = "2026-05-06T19:22:00.835Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a4/14cc0768164dd53bec48aa41a20270b18df9bf72aa5054278bf133608315/mypy-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:33f9cf4825469b2bc73c53ba55f6d9a9b4cdb60f9e6e228745581520f29b8771", size = 14635914, upload-time = "2026-05-06T19:23:43.675Z" }, + { url = "https://files.pythonhosted.org/packages/08/48/d866a3e23b4dc5974c77d9cf65a435bf22de01a84dd4620917950e233960/mypy-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191675c3c7dc2a5c7722a035a6909c277f14046c5e4e02aa5fbf65f8524f08ad", size = 15270866, upload-time = "2026-05-06T19:22:34.756Z" }, + { url = "https://files.pythonhosted.org/packages/71/eb/de9ef94958eb2078a6b908ceb247757dc384d3a238d3bd6ed7d81de5eaf8/mypy-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3d26c4321a3b06fc9f04c741e0733af693f82d823f8e64e47b2e63b7f19fa84", size = 16093131, upload-time = "2026-05-06T19:23:56.541Z" }, + { url = "https://files.pythonhosted.org/packages/ad/07/0ab2c1a9d26e90942612724cbd5788f16b7810c5dd39bfcf79286c6c4524/mypy-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bbcbc4d5917ca6ce12de70e051de7f533e3bf92d548b41a38a2232a6fe356525", size = 16330685, upload-time = "2026-05-06T19:21:42.037Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8f/46f85d1371a5be642dad263828118ae1efd536d91d8bd2000c68acff3920/mypy-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:dbc6ba6d40572ae49268531565793a8f07eac7fc65ad76d482c9b4c8765b6043", size = 12752017, upload-time = "2026-05-06T19:22:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e6/94ca48800cac19eb28a58188a768aaec0d16cac0f373915f073058ab0855/mypy-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:77926029dfcb7e1a3ecb0acb2ddbb24ca36be03f7d623e1759ad5376be8f6c01", size = 10527097, upload-time = "2026-05-06T19:20:58.973Z" }, + { url = "https://files.pythonhosted.org/packages/5c/14/fd0694aa594d6e9f9fd16ce821be2eff295197a273262ef56ddcc1388d68/mypy-2.0.0-py3-none-any.whl", hash = "sha256:8a92b2be3146b4fa1f062af7eb05574cbf3e6eb8e1f14704af1075423144e4e5", size = 2673434, upload-time = "2026-05-06T19:26:32.856Z" }, ] [[package]] @@ -4136,7 +4175,7 @@ wheels = [ [[package]] name = "onnxruntime" -version = "1.25.1" +version = "1.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flatbuffers" }, @@ -4145,25 +4184,24 @@ dependencies = [ { name = "protobuf" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/52/8b2a10e8dedf5d486332bc2b3bca0b1ed8049c0b9e4a5cced95413aadfdd/onnxruntime-1.25.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:66e52f7a30d1f780a34aa84d68a0a04d382d9f5b141884ecbf45b7566b9fbde9", size = 17770987, upload-time = "2026-04-27T22:00:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/3f/87/a424d2867477c42ef8c60172709281120797f7b0f1fd33cc36b24329c825/onnxruntime-1.25.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5f41779f044d1ff75593df5c10a4d311bc82563687796d5218e2685b8f9da25", size = 15871829, upload-time = "2026-04-27T21:59:39.088Z" }, - { url = "https://files.pythonhosted.org/packages/d4/55/7819e64c515f17c86005447ede8122b974ca851255a94125e2119376f0f8/onnxruntime-1.25.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:905409e9eb2ef87f8226e073f56e71faf731c3e480ebd34952cf953730e4a4ff", size = 18024586, upload-time = "2026-04-27T22:00:05.359Z" }, - { url = "https://files.pythonhosted.org/packages/89/36/b4f3eb5e95c66389aafd490950b5255e87c9333742cf90516eb50898e1dc/onnxruntime-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4097b75b77486bb45835a8ed25b9a67976040ec6c258aeabae6aadfbdd1201c", size = 12905112, upload-time = "2026-04-27T22:00:36.478Z" }, - { url = "https://files.pythonhosted.org/packages/38/fa/e5c43397632a399f542663ed3e3e37763ee203ba845b10b266cd2ede8925/onnxruntime-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:b6c7aa5cae606d5c90a392679fac074b60f80025a2e83e1e90fdf882bd2a97f0", size = 12634433, upload-time = "2026-04-27T22:00:25.918Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ee/db3ac55ef770347a926ac0f1317df0ab42c8bc604350833b30c7356bf936/onnxruntime-1.25.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e9d9b3b1694196bc3c5bc66f760a237a5e27d7688aaa2e2c9c0f66abd0486699", size = 17770761, upload-time = "2026-04-27T21:59:54.853Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9a/33225481a94a59906fce44e27ab12fc3bddd2aaecdc6160bd73341ca1aba/onnxruntime-1.25.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:311d29b943e46a55ca72ca1ea48d7815c993122bfc359f68215fddeb9583fff4", size = 15871542, upload-time = "2026-04-27T21:59:41.881Z" }, - { url = "https://files.pythonhosted.org/packages/8b/09/f20aac60f6fcf840543be54d4e9252cfeb7e8c2bb6d22477aaeb180e763e/onnxruntime-1.25.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98016a038b31160db23208706139fa3b99cd60bc1c5ffdade77aafd6a37a92ad", size = 18036960, upload-time = "2026-04-27T22:00:10.739Z" }, - { url = "https://files.pythonhosted.org/packages/50/83/47964ac7e2f7e2f9e83c69ec466642c6835466252cc2ef0561eafeb56b66/onnxruntime-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:08717d6eee2820807ba60b1b17032af207bd7aaca5b6c4abaee71f83feae877b", size = 12904886, upload-time = "2026-04-27T22:00:39.878Z" }, - { url = "https://files.pythonhosted.org/packages/d4/6c/a6c5aea47dc95fca7728f8a5af67c184ec9e7d4e7882125c7062e4bba8dd/onnxruntime-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:84f8963d70e00167bae273ab7e80e9795bfc5eb94f6b23236a99c5c11af00844", size = 12634117, upload-time = "2026-04-27T22:00:29.15Z" }, - { url = "https://files.pythonhosted.org/packages/a8/8a/3b65e7911eec86c125e3d6f43d690a6f68671500543c0390ecd6eb59b771/onnxruntime-1.25.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03e800b3a4b48d9f3a2d23aacc4fa95486a3b406b14e51d1a9b8b6981d9adf9c", size = 15882935, upload-time = "2026-04-27T21:59:44.912Z" }, - { url = "https://files.pythonhosted.org/packages/3c/bb/410a760694f8ae7bbfc5fa81ccbeb7da241e6d520ee02a333a439cf462a2/onnxruntime-1.25.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd83ef5c10cfc051a1cb465db692d57b996a1bc75a2a97b161398e29cdbc47ff", size = 18021727, upload-time = "2026-04-27T22:00:13.846Z" }, - { url = "https://files.pythonhosted.org/packages/fb/aa/04530bd38e31e26970fa1212346d76cf81705dc16a8ee5e6f4fb24634c11/onnxruntime-1.25.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:395eb662c437fa2407f44266e4778b75bff261b17c2a6fef042421f9069f871d", size = 17773721, upload-time = "2026-04-27T21:59:59.24Z" }, - { url = "https://files.pythonhosted.org/packages/ef/7f/ec79ab5cece6a688c944a7fa214a8511d548b9d5142a15d1a3d730b705f1/onnxruntime-1.25.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ae85395f41b291ae3e61780ec5092640181d369ef6c268aa8141c478b509e69", size = 15875954, upload-time = "2026-04-27T21:59:49.394Z" }, - { url = "https://files.pythonhosted.org/packages/67/fe/20428215d822099ea2c1e3cf35c295cf1a58f467bf18b6c607597a39c18a/onnxruntime-1.25.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:828e1b12710fbedb6dfab5e7bae6f11563617cddf3c2e7e8d84c64de566a4a3a", size = 18038703, upload-time = "2026-04-27T22:00:16.199Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b1/b15db965e6a68bc47ca7eb584de4e6b3d2d2f484d46cc57f715b596f6528/onnxruntime-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:2affc9d2fd9ab013b9c9637464e649a0cca870d57ae18bfef74180eee65c3369", size = 13218513, upload-time = "2026-04-27T22:00:42.506Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f9/25cd2d1b29cdc8140eee4afbb6fb930b69125526632b1d579bc747975306/onnxruntime-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:3387d75d1a815b4b2495b4e47a05ef1b3bcb64a817ddc68587e0bfcb9702bcf6", size = 12969835, upload-time = "2026-04-27T22:00:31.504Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0e/6c507d1e65b2421fb44e241cbba577c7276792279485024fb1752b43f5c5/onnxruntime-1.25.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06280b06604660595037f783c6d24bc70cbe5c6093975f194cd1482e77d450de", size = 15883298, upload-time = "2026-04-27T21:59:51.991Z" }, - { url = "https://files.pythonhosted.org/packages/df/4e/1c9df57496409dc86b320bd38f29ad7a34b7115e4f35b8fca44a827568a7/onnxruntime-1.25.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e79fd5ce7db10ebcc24e020e2ed0159476e69e2326b9b7828e5aadcf6184212", size = 18021249, upload-time = "2026-04-27T22:00:18.954Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a0/3f9d896a0385a36bd04345d6d0b802821a5782adde562e7e135f6bb71c73/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91f2bb870a4b9224eba0a6728c1fa7a9e552b8e59e1083c51fbbc3d013f2b5c0", size = 16052692, upload-time = "2026-05-08T19:07:13.829Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/2a4e04f8dbeffad19bbcced4bcd4289bf478921518437404d6b92bdf213b/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b6dd70599005bd1bf29779f04a91978b92b5e719c11a20068a8f8e535f725b6", size = 18185439, upload-time = "2026-05-08T19:07:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/44/fc/026d0a7162b9c2153dac292baea9e027c42304dc1d9dc6f8ff5b4cfbaedd/onnxruntime-1.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:a26374dc7fbcaae593601086b242120e13f2310558df0991da6dd8b8fac00414", size = 13026427, upload-time = "2026-05-08T19:08:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/3e/27/1dcf88e45e4c69db5f7b106f2dacc3801ba98994e082ca03e1dfdf7bfe57/onnxruntime-1.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:54a8053410fd31fd66469bd754fcfe8a4df9f7eb44756b4b5479bf50c842d948", size = 12796647, upload-time = "2026-05-08T19:07:52.108Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a2/c801242685e0ce48a4ca51dfafbb588765e0446397e123be53ba5598f3f5/onnxruntime-1.26.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccce19c5f771b8268902f77d9fed9e88f9499465d6780808faa6611a789d33f0", size = 18016563, upload-time = "2026-05-08T19:07:28.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/64/0492c0b1db04e29b2630c87cfa36f9d6872b1ca8614b90c5cad58fac7d76/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdbed8cf3b672b66acb032f33a253bc27f42bce6ece48ae3fab4fa483a5e96e0", size = 16052634, upload-time = "2026-05-08T19:07:16.885Z" }, + { url = "https://files.pythonhosted.org/packages/3d/26/4d09ddc755a84fc8d5e192991626b0e0680e8f6c5d58f4f1d05c42bc48cf/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07af6fc6d5557835f2b6ee7a96d8b3235d0c57a8e230efdedaee106a8a3cbc6", size = 18185632, upload-time = "2026-05-08T19:07:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/3e52249aa08fa301e217ecba07b5246a8338fa2b401e109326e3fc5be0f9/onnxruntime-1.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:61bec80655efa460591c2bc655392d57d2650ce85533a6b9b3b7a790d7ea7916", size = 13026751, upload-time = "2026-05-08T19:08:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/06/b3/c1c8782b14af6797c303de132d6eef26a9fb80dfacd3750ce57911d11c6b/onnxruntime-1.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a6677545ff451e3539a02746d2f207d8c5baa4a0a818886bb9d6a6eb9511ee89", size = 12796807, upload-time = "2026-05-08T19:07:54.879Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f5/47b0676408abec652c14b84d7173e389837832d850c24f87184277313e8d/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e016edc15d3c19f36807e1c6b10be5b27807688c32720f91b5ae480a95215d0", size = 16057265, upload-time = "2026-05-08T19:07:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/3b/45/33ab6deeef010ca844c877dd618cebc079590bbe52d2a3678e7223b1b908/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5fc48a91a046a6a5c9b147f83fb41d65d24d24923373b222cdd248f0f4f4aac", size = 18197590, upload-time = "2026-05-08T19:07:41.422Z" }, + { url = "https://files.pythonhosted.org/packages/40/89/17546c1c20f6bfc3ae41c22152378a26edfea918af3129e2139dcd7c99f3/onnxruntime-1.26.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:33a791f31432a3af1a96db5e54818b37aba5e5eefc2e6af5794c10a9118a9993", size = 18019724, upload-time = "2026-05-08T19:07:30.723Z" }, + { url = "https://files.pythonhosted.org/packages/bb/24/89457a35f6af29538a76647f2c18c3a28277e6c19234c847e7b4b7c19860/onnxruntime-1.26.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e90c00732c4553618103149d93f688e8c3063017938f8983e21a71d9f3b6d22e", size = 16054821, upload-time = "2026-05-08T19:07:22.348Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/15b2e1815cf570d238e0135529f80d2dce64e8e8818a1489cae83823c5c6/onnxruntime-1.26.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01498e80ba8988428d08c2d51b1338f89e3de2a93e6ffe555f79c68f26a5c06b", size = 18185815, upload-time = "2026-05-08T19:07:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/2e11055faf015e4b07f45b513fa49b391baf2e19d92d77d73ebee13c1004/onnxruntime-1.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:7ead61450d8405167c87dd3a31d8da1d576b490a57dab1aa8b82a7da6825f5aa", size = 13349887, upload-time = "2026-05-08T19:08:08.671Z" }, + { url = "https://files.pythonhosted.org/packages/19/e4/0f9d1a5718b1781c610c1e354765a3820597081754277a6a9a2b50705702/onnxruntime-1.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:31d71a53490e46910877d0902b5ad99c69a5955e5c7ea6c82863519410e1ba7c", size = 13140121, upload-time = "2026-05-08T19:07:57.804Z" }, + { url = "https://files.pythonhosted.org/packages/1c/42/3b8e635f067d06d9f45bede470b8d539d101a4166c272213158dfd08b6ce/onnxruntime-1.26.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b6d258fb78fdfcf049795bcfaa74dcb90ae7baa277afd21e6fd28b83f2c496", size = 16057240, upload-time = "2026-05-08T19:07:25.163Z" }, + { url = "https://files.pythonhosted.org/packages/93/99/f2be40a31b908d96b861ae0ce98582fa376c18a7f816b9d5eb4cd6aa0a4c/onnxruntime-1.26.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4eefd386a45202aefb7a5132b94f32df9d506c9edcc7faf2fc60d65183f4b183", size = 18197382, upload-time = "2026-05-08T19:07:46.965Z" }, ] [[package]] @@ -4499,86 +4537,96 @@ wheels = [ [[package]] name = "propcache" -version = "0.4.1" +version = "0.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] @@ -4695,7 +4743,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.3" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -4703,84 +4751,84 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.3" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, - { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, - { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, - { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, - { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, - { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, - { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, - { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, - { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, - { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, - { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, - { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, - { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, - { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, - { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, - { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, - { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, - { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, - { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, - { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, - { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, - { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, - { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, - { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, - { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, - { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, - { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, - { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, - { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, - { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, - { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, - { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, - { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, - { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, - { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, - { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, - { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, - { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, - { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, - { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, - { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, - { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, - { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, - { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] [[package]] @@ -5084,15 +5132,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.2.2" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/cc5a8653e9a24f6cf84768f05064aa8ed5a83dcefd5e2a043db14a1c5f44/python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0", size = 63925, upload-time = "2026-05-05T14:38:39.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f", size = 33124, upload-time = "2026-05-05T14:38:38.539Z" }, ] [[package]] @@ -5316,90 +5364,90 @@ wheels = [ [[package]] name = "regex" -version = "2026.4.4" +version = "2026.5.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/28/b972a4d3df61e1d7bcf1b59fdb3cddef22f88b6be43f161bb41ebc0e4081/regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52", size = 490434, upload-time = "2026-04-03T20:53:40.219Z" }, - { url = "https://files.pythonhosted.org/packages/84/20/30041446cf6dc3e0eab344fc62770e84c23b6b68a3b657821f9f80cb69b4/regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb", size = 292061, upload-time = "2026-04-03T20:53:41.862Z" }, - { url = "https://files.pythonhosted.org/packages/62/c8/3baa06d75c98c46d4cc4262b71fd2edb9062b5665e868bca57859dadf93a/regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76", size = 289628, upload-time = "2026-04-03T20:53:43.701Z" }, - { url = "https://files.pythonhosted.org/packages/31/87/3accf55634caad8c0acab23f5135ef7d4a21c39f28c55c816ae012931408/regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be", size = 796651, upload-time = "2026-04-03T20:53:45.379Z" }, - { url = "https://files.pythonhosted.org/packages/f6/0c/aaa2c83f34efedbf06f61cb1942c25f6cf1ee3b200f832c4d05f28306c2e/regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1", size = 865916, upload-time = "2026-04-03T20:53:47.064Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f6/8c6924c865124643e8f37823eca845dc27ac509b2ee58123685e71cd0279/regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13", size = 912287, upload-time = "2026-04-03T20:53:49.422Z" }, - { url = "https://files.pythonhosted.org/packages/11/0e/a9f6f81013e0deaf559b25711623864970fe6a098314e374ccb1540a4152/regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9", size = 801126, upload-time = "2026-04-03T20:53:51.096Z" }, - { url = "https://files.pythonhosted.org/packages/71/61/3a0cc8af2dc0c8deb48e644dd2521f173f7e6513c6e195aad9aa8dd77ac5/regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d", size = 776788, upload-time = "2026-04-03T20:53:52.889Z" }, - { url = "https://files.pythonhosted.org/packages/64/0b/8bb9cbf21ef7dee58e49b0fdb066a7aded146c823202e16494a36777594f/regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3", size = 785184, upload-time = "2026-04-03T20:53:55.627Z" }, - { url = "https://files.pythonhosted.org/packages/99/c2/d3e80e8137b25ee06c92627de4e4d98b94830e02b3e6f81f3d2e3f504cf5/regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0", size = 859913, upload-time = "2026-04-03T20:53:57.249Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/9d5d876157d969c804622456ef250017ac7a8f83e0e14f903b9e6df5ce95/regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043", size = 765732, upload-time = "2026-04-03T20:53:59.428Z" }, - { url = "https://files.pythonhosted.org/packages/82/80/b568935b4421388561c8ed42aff77247285d3ae3bb2a6ca22af63bae805e/regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244", size = 852152, upload-time = "2026-04-03T20:54:01.505Z" }, - { url = "https://files.pythonhosted.org/packages/39/29/f0f81217e21cd998245da047405366385d5c6072048038a3d33b37a79dc0/regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73", size = 789076, upload-time = "2026-04-03T20:54:03.323Z" }, - { url = "https://files.pythonhosted.org/packages/49/1d/1d957a61976ab9d4e767dd4f9d04b66cc0c41c5e36cf40e2d43688b5ae6f/regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f", size = 266700, upload-time = "2026-04-03T20:54:05.639Z" }, - { url = "https://files.pythonhosted.org/packages/c5/5c/bf575d396aeb58ea13b06ef2adf624f65b70fafef6950a80fc3da9cae3bc/regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b", size = 277768, upload-time = "2026-04-03T20:54:07.312Z" }, - { url = "https://files.pythonhosted.org/packages/c9/27/049df16ec6a6828ccd72add3c7f54b4df029669bea8e9817df6fff58be90/regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983", size = 270568, upload-time = "2026-04-03T20:54:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, - { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, - { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, - { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, - { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, - { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, - { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, - { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, - { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, - { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, - { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, - { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, - { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, - { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, - { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, - { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, - { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, - { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, - { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, - { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, - { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, - { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, - { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, - { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, - { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, - { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, - { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, - { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, - { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, - { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, - { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, - { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, - { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, - { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, - { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, - { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, - { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, - { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, - { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, - { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, - { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, + { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, + { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, + { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, + { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, + { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, ] [[package]] @@ -6030,7 +6078,7 @@ wheels = [ [[package]] name = "timm" -version = "1.0.26" +version = "1.0.27" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, @@ -6039,9 +6087,9 @@ dependencies = [ { name = "torch" }, { name = "torchvision" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/1e/e924b3b2326a856aaf68586f9c52a5fc81ef45715eca408393b68c597e0e/timm-1.0.26.tar.gz", hash = "sha256:f66f082f2f381cf68431c22714c8b70f723837fa2a185b155961eab90f2d5b10", size = 2419859, upload-time = "2026-03-23T18:12:10.272Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/54/ece85b0eef3700c90db8271a43669b05a0ebbe2edb1962329c34374a297e/timm-1.0.27.tar.gz", hash = "sha256:315dfe63186ca9fb7ff941268941231fd5be259f2b4bb4afa28560ae1015cb9a", size = 2439861, upload-time = "2026-05-08T19:38:36.844Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/e9/bebf3d50e3fc847378988235f87c37ad3ac26d386041ab915d15e92025cd/timm-1.0.26-py3-none-any.whl", hash = "sha256:985c330de5ccc3a2aa0224eb7272e6a336084702390bb7e3801f3c91603d3683", size = 2568766, upload-time = "2026-03-23T18:12:08.062Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2e/26bab7686ff4aed48f8f5f6c23e2aa37b7a37ddd9effe3aa61e908fd518f/timm-1.0.27-py3-none-any.whl", hash = "sha256:5ff07c9ddf53cbada88eab1c93ff175c64cab683b5a2fddf863bcee985926f89", size = 2589280, upload-time = "2026-05-08T19:38:35.034Z" }, ] [[package]] @@ -6240,11 +6288,11 @@ wheels = [ [[package]] name = "traitlets" -version = "5.14.3" +version = "5.15.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/22/40f55b26baeab80c2d7b3f1db0682f8954e4617fee7d90ce634022ef05c6/traitlets-5.15.0.tar.gz", hash = "sha256:4fead733f81cf1c4c938e06f8ca4633896833c9d89eff878159457f4d4392971", size = 163197, upload-time = "2026-05-06T08:05:58.016Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/da/98/a9937a969d018a23badfea0b381f66783649d48e0ea6c41923265c3cbeb3/traitlets-5.15.0-py3-none-any.whl", hash = "sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40", size = 85877, upload-time = "2026-05-06T08:05:55.853Z" }, ] [[package]] @@ -6369,11 +6417,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] From 1f7b03f5f218b8531dd3d3787ae5cd3e9d239a67 Mon Sep 17 00:00:00 2001 From: Anthony Shoumikhin Date: Sun, 10 May 2026 04:05:35 -0700 Subject: [PATCH 04/18] chore(deps): allow torch 2.11/2.12 and fix autocast deprecation (#3435) * chore(deps): allow torch 2.11/2.12 and fix autocast deprecation - Bump torch to >=2.7,<2.13 (was <2.11), torchvision to <0.28 (was <0.26), and torchcodec to <0.13 (was <0.11) to allow installs against the latest stable torch 2.11 and the upcoming 2.12 line. - Replace removed torch.get_autocast_gpu_dtype() with torch.get_autocast_dtype("cuda") in Florence2 and Qwen2.5-VL-MoE FlashAttention paths (the former is removed in 2.11+). - Refresh uv.lock for the new resolution (torch 2.11.0+cu130, torchvision 0.26.0+cu130, torchcodec 0.11.1, full CUDA 13 stack). Verified locally with `uv sync --locked` from a clean .venv and the lerobot test suite (pytest -n 8 --dist=loadfile --timeout=300). Failure set is identical to the pre-bump baseline: 18 pre-existing failures (test_sac_policy*, test_pi0_rtc*, test_pi05_rtc*, test_replay_buffer*), 0 new, 0 fixed. AI assistance: this change was authored with Claude Code per AI_POLICY.md. * fix(policies): use device-agnostic autocast dtype lookup Pass query_states.device.type to torch.get_autocast_dtype() instead of hardcoding 'cuda', so the cast matches the active autocast context when running under CPU/MPS/XPU autocast. --------- Co-authored-by: Steven Palma --- pyproject.toml | 6 +- .../wall_x/qwen_model/qwen2_5_vl_moe.py | 2 +- .../policies/xvla/modeling_florence2.py | 2 +- uv.lock | 342 ++++++++++-------- 4 files changed, 202 insertions(+), 150 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0ae3abd73..7f9ad2141 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,8 +59,8 @@ keywords = ["lerobot", "huggingface", "robotics", "machine learning", "artifici dependencies = [ # Core ML - "torch>=2.7,<2.11.0", - "torchvision>=0.22.0,<0.26.0", + "torch>=2.7,<2.13.0", + "torchvision>=0.22.0,<0.28.0", "numpy>=2.0.0,<2.3.0", # NOTE: Explicitly listing numpy helps the resolver converge faster. Upper bound imposed by opencv-python-headless. "opencv-python-headless>=4.9.0,<4.14.0", "Pillow>=10.0.0,<13.0.0", @@ -99,7 +99,7 @@ dataset = [ "pandas>=2.0.0,<3.0.0", # NOTE: Transitive dependency of datasets "pyarrow>=21.0.0,<30.0.0", # NOTE: Transitive dependency of datasets "lerobot[av-dep]", - "torchcodec>=0.3.0,<0.11.0; sys_platform != 'win32' and (sys_platform != 'linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l')) and (sys_platform != 'darwin' or platform_machine != 'x86_64')", # NOTE: Windows support starts at version 0.7 (needs torch==2.8), ffmpeg>=8 support starts at version 0.8.1 (needs torch==2.9), system-wide ffmpeg support starts at version 0.10 (needs torch==2.10). + "torchcodec>=0.3.0,<0.13.0; sys_platform != 'win32' and (sys_platform != 'linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l')) and (sys_platform != 'darwin' or platform_machine != 'x86_64')", # NOTE: Windows support starts at version 0.7 (needs torch==2.8), ffmpeg>=8 support starts at version 0.8.1 (needs torch==2.9), system-wide ffmpeg support starts at version 0.10 (needs torch==2.10), 0.11 needs torch==2.11, 0.12 needs torch==2.12. "jsonlines>=4.0.0,<5.0.0", ] training = [ diff --git a/src/lerobot/policies/wall_x/qwen_model/qwen2_5_vl_moe.py b/src/lerobot/policies/wall_x/qwen_model/qwen2_5_vl_moe.py index a80096514..fd60f976f 100644 --- a/src/lerobot/policies/wall_x/qwen_model/qwen2_5_vl_moe.py +++ b/src/lerobot/policies/wall_x/qwen_model/qwen2_5_vl_moe.py @@ -939,7 +939,7 @@ class Qwen2_5_VLFlashAttention2(Qwen2_5_VLAttention): input_dtype = query_states.dtype if input_dtype == torch.float32: if torch.is_autocast_enabled(): - target_dtype = torch.get_autocast_gpu_dtype() + target_dtype = torch.get_autocast_dtype(query_states.device.type) # Handle the case where the model is quantized elif hasattr(self.config, "_pre_quantization_dtype"): target_dtype = self.config._pre_quantization_dtype diff --git a/src/lerobot/policies/xvla/modeling_florence2.py b/src/lerobot/policies/xvla/modeling_florence2.py index 81f9c8234..ccf48e29f 100644 --- a/src/lerobot/policies/xvla/modeling_florence2.py +++ b/src/lerobot/policies/xvla/modeling_florence2.py @@ -985,7 +985,7 @@ class Florence2FlashAttention2(Florence2Attention): input_dtype = query_states.dtype if input_dtype == torch.float32: if torch.is_autocast_enabled(): - target_dtype = torch.get_autocast_gpu_dtype() + target_dtype = torch.get_autocast_dtype(query_states.device.type) # Handle the case where the model is quantized elif hasattr(self.config, "_pre_quantization_dtype"): target_dtype = self.config._pre_quantization_dtype diff --git a/uv.lock b/uv.lock index bd5036a9e..e2c08bf3e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.15' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", @@ -995,17 +995,20 @@ wheels = [ [[package]] name = "cuda-bindings" -version = "12.9.4" +version = "13.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux'" }, + { name = "cuda-pathfinder", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, - { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, - { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, - { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, ] [[package]] @@ -1016,6 +1019,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, ] +[[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'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -1085,7 +1131,7 @@ name = "decord" version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l') or sys_platform != 'linux'" }, + { name = "numpy", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x') or (platform_machine != 's390x' and sys_platform != 'linux')" }, ] 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" }, @@ -3148,10 +3194,10 @@ requires-dist = [ { name = "teleop", marker = "extra == 'phone'", specifier = ">=0.1.0,<0.2.0" }, { name = "termcolor", specifier = ">=2.4.0,<4.0.0" }, { name = "timm", marker = "extra == 'groot'", specifier = ">=1.0.0,<1.1.0" }, - { name = "torch", specifier = ">=2.7,<2.11.0" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux' and extra == 'dataset') or (platform_machine != 'x86_64' and sys_platform == 'darwin' and extra == 'dataset') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'dataset')", specifier = ">=0.3.0,<0.11.0" }, + { name = "torch", specifier = ">=2.7,<2.13.0" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux' and extra == 'dataset') or (platform_machine != 'x86_64' and sys_platform == 'darwin' and extra == 'dataset') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'dataset')", specifier = ">=0.3.0,<0.13.0" }, { name = "torchdiffeq", marker = "extra == 'wallx'", specifier = ">=0.2.4,<0.3.0" }, - { name = "torchvision", specifier = ">=0.22.0,<0.26.0" }, + { name = "torchvision", specifier = ">=0.22.0,<0.28.0" }, { name = "tqdm", specifier = ">=4.66.0,<5.0.0" }, { name = "transformers", marker = "extra == 'transformers-dep'", specifier = ">=5.4.0,<5.6.0" }, { name = "wandb", marker = "extra == 'training'", specifier = ">=0.24.0,<0.25.0" }, @@ -3997,137 +4043,152 @@ wheels = [ ] [[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 = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux'" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, + { url = "https://files.pythonhosted.org/packages/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 = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] 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 = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux'" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, + { url = "https://files.pythonhosted.org/packages/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 = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, + { 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-cusparselt-cu12" -version = "0.7.1" +name = "nvidia-cusparselt-cu13" +version = "0.8.0" 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/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" } 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]] @@ -4935,10 +4996,10 @@ name = "pyobjc-framework-applicationservices" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, - { name = "pyobjc-framework-cocoa", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, - { name = "pyobjc-framework-coretext", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, - { name = "pyobjc-framework-quartz", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, + { name = "pyobjc-core", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "pyobjc-framework-cocoa", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "pyobjc-framework-coretext", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "pyobjc-framework-quartz", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/6a/d4e613c8e926a5744fc47a9e9fea08384a510dc4f27d844f7ad7a2d793bd/pyobjc_framework_applicationservices-12.1.tar.gz", hash = "sha256:c06abb74f119bc27aeb41bf1aef8102c0ae1288aec1ac8665ea186a067a8945b", size = 103247, upload-time = "2025-11-14T10:08:52.18Z" } wheels = [ @@ -4954,7 +5015,7 @@ name = "pyobjc-framework-cocoa" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, + { name = "pyobjc-core", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } wheels = [ @@ -4970,9 +5031,9 @@ name = "pyobjc-framework-coretext" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, - { name = "pyobjc-framework-cocoa", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, - { name = "pyobjc-framework-quartz", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, + { name = "pyobjc-core", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "pyobjc-framework-cocoa", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "pyobjc-framework-quartz", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/da/682c9c92a39f713bd3c56e7375fa8f1b10ad558ecb075258ab6f1cdd4a6d/pyobjc_framework_coretext-12.1.tar.gz", hash = "sha256:e0adb717738fae395dc645c9e8a10bb5f6a4277e73cba8fa2a57f3b518e71da5", size = 90124, upload-time = "2025-11-14T10:14:38.596Z" } wheels = [ @@ -4988,8 +5049,8 @@ name = "pyobjc-framework-quartz" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, - { name = "pyobjc-framework-cocoa", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, + { name = "pyobjc-core", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "pyobjc-framework-cocoa", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } wheels = [ @@ -6141,75 +6202,61 @@ wheels = [ [[package]] name = "torch" -version = "2.10.0" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { 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" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "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 = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "triton", marker = "sys_platform == 'linux'" }, { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, - { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, - { url = "https://files.pythonhosted.org/packages/f4/39/590742415c3030551944edc2ddc273ea1fdfe8ffb2780992e824f1ebee98/torch-2.10.0-3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328", size = 915632474, upload-time = "2026-03-11T14:15:13.666Z" }, - { url = "https://files.pythonhosted.org/packages/b6/8e/34949484f764dde5b222b7fe3fede43e4a6f0da9d7f8c370bb617d629ee2/torch-2.10.0-3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591", size = 915523882, upload-time = "2026-03-11T14:14:46.311Z" }, - { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, - { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, - { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, - { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, - { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, - { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, - { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, - { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, - { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" }, - { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, - { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" }, - { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" }, - { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" }, - { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" }, - { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" }, - { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, + { url = "https://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" }, + { url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" }, + { url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" }, + { url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" }, + { url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" }, + { url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" }, + { url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, ] [[package]] name = "torchcodec" -version = "0.10.0" +version = "0.11.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/ff/2b27797e039673156710e5a0febe87cafc203722acafa3d34db283b40cf9/torchcodec-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b35fa4061c5757f8d714187c040a90a11669de6470a644bb04e3cd335ff1c110", size = 4073213, upload-time = "2026-01-22T15:41:45.485Z" }, - { url = "https://files.pythonhosted.org/packages/29/34/ccc711b6dc581e43b8d8d227e4173a8826994ee7b68d6b3d82291f307325/torchcodec-0.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6e43184d83ccced965b31cad5bb6200c779646fee2ec153a6d784b4def40c91b", size = 2083121, upload-time = "2026-01-22T15:41:37.947Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b6/b1041c8ccb175b08779b3e2d3e60f838bbcbfe2398d49e3673b6a66f0649/torchcodec-0.10.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:be3ce7cc667effecd06da9d0d6c5e9e347c5f376b705934e7b82378a65cf6eef", size = 4043681, upload-time = "2026-01-22T15:41:47.236Z" }, - { url = "https://files.pythonhosted.org/packages/fd/85/fc44f6d702dfd344e6859a9a4d713aaaa991578eb74677a80297d9ae8a07/torchcodec-0.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:71f25caf9ab89a434ae2008b1374fd98557a6864b8313b103bae53af3e6fd17f", size = 2088572, upload-time = "2026-01-22T15:41:39.203Z" }, - { url = "https://files.pythonhosted.org/packages/90/c9/4b6242e3456bae148f4086337d3e43d98c4e79c04091de3462db9f5eb67a/torchcodec-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bb882c12ca07dcf6d82833db67e6b565693a4bccfeab6696697620e43e465556", size = 3821202, upload-time = "2026-01-22T15:41:48.627Z" }, - { url = "https://files.pythonhosted.org/packages/96/a2/5fe0e62b208a367f741361881321c1b25de487318a44f870f326747585a1/torchcodec-0.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:bec2b938ad4b294bd71d0b0ab4976037c740be0c80be79e67803ebed4eff270e", size = 2089647, upload-time = "2026-01-22T15:41:40.711Z" }, + { url = "https://files.pythonhosted.org/packages/64/85/38f4843ff2a6bf7dfb71a153acd99024dadb96749965a67524c2f1cc1894/torchcodec-0.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:57056e91d1d883d0fb77ca7759e304be9c0bdb4ea0e37bde5c2e361347063b8c", size = 4368988, upload-time = "2026-04-14T18:24:51.46Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/3b41034b0f1289423745f918ace2a1e1e86b9c578c2e2461b6afcbb5354a/torchcodec-0.11.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f1aee486a84247fcaa67870ac5005aa8d382a9839e91e476fa71b5b3d9fda9b7", size = 2397532, upload-time = "2026-04-14T18:24:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a9/a2b6ee3e84c55bdd0c45fd991dde71c95a99115ec9e26938b212b4545dcf/torchcodec-0.11.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6c26e90e7aa982302644d0af8cb706318682bb390f48a80ecbfeab03499acd04", size = 2329883, upload-time = "2026-04-14T18:24:55.467Z" }, + { url = "https://files.pythonhosted.org/packages/2c/61/a8985a7561ef651e409deeac151a0ed5cef763db9577db5cc49c2f5eaab2/torchcodec-0.11.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:915fbe20068ec77486fbbeaf0c627c89c7376445f27d215b7489c0a03c64fd4c", size = 4289805, upload-time = "2026-04-14T18:24:59.124Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/c4ec0304dd169a9b2b7fa0dd1d5d659d3cccc975b98ac88c498fe6dd7196/torchcodec-0.11.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3755de03c96afd37410cba68198225d11cd6431a32f2161a0019791a4a853305", size = 2399057, upload-time = "2026-04-14T18:25:00.782Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b2/85ad7a81f387e40983c21bc94da0c333974afb41f38c3a85d25875274187/torchcodec-0.11.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5eee69971cec1147a03b8a6b678b5dfbeff0b2c71ed7929e488391f9fbcd630c", size = 2332721, upload-time = "2026-04-14T18:25:02.518Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b7/8d6ee76fca0cfefec01402f33c11766455da2b8460cb9191cdc34f8defc0/torchcodec-0.11.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:a00ef79e847644f91c9995de021062adc851916b16244d26c0a7a04569710508", size = 4408290, upload-time = "2026-04-14T18:25:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1e/e37bd46ffac9eec1a9afc32c5097cd83b0de1e865021f7f953c5142919f4/torchcodec-0.11.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:170a3efea64f0cd2c21cee0a233a9e13c67a704b5c5e7ef9aeda31e747ac6885", size = 2402232, upload-time = "2026-04-14T18:25:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d0/a9173dbfa011cc2224f7489e50844b9f62110050bbdbd9d29485e7f1e0e2/torchcodec-0.11.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:db66ddce36a6fa35f30fbe1d78b57289fcb53f8f43c1c85923edbe339540c665", size = 2334158, upload-time = "2026-04-14T18:25:09.77Z" }, ] [[package]] @@ -6227,7 +6274,7 @@ wheels = [ [[package]] name = "torchvision" -version = "0.25.0" +version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -6235,26 +6282,26 @@ dependencies = [ { name = "torch" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" }, - { url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/16/8f650c2e288977cf0f8f85184b90ee56ed170a4919347fc74ee99286ed6f/torchvision-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3", size = 4303059, upload-time = "2026-01-21T16:27:11.08Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" }, - { url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" }, - { url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/32/a5/9a9b1de0720f884ea50dbf9acb22cbe5312e51d7b8c4ac6ba9b51efd9bba/torchvision-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f", size = 4321911, upload-time = "2026-01-21T16:27:35.195Z" }, - { url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" }, - { url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" }, - { url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" }, - { url = "https://files.pythonhosted.org/packages/63/cc/0ea68b5802e5e3c31f44b307e74947bad5a38cc655231d845534ed50ddb8/torchvision-0.25.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c", size = 4344260, upload-time = "2026-01-21T16:27:17.018Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1f/fa839532660e2602b7e704d65010787c5bb296258b44fa8b9c1cd6175e7d/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1", size = 1896193, upload-time = "2026-01-21T16:27:24.785Z" }, - { url = "https://files.pythonhosted.org/packages/80/ed/d51889da7ceaf5ff7a0574fb28f9b6b223df19667265395891f81b364ab3/torchvision-0.25.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce", size = 2309331, upload-time = "2026-01-21T16:27:19.97Z" }, - { url = "https://files.pythonhosted.org/packages/90/a5/f93fcffaddd8f12f9e812256830ec9c9ca65abbf1bc369379f9c364d1ff4/torchvision-0.25.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03", size = 8088713, upload-time = "2026-01-21T16:27:15.281Z" }, - { url = "https://files.pythonhosted.org/packages/1f/eb/d0096eed5690d962853213f2ee00d91478dfcb586b62dbbb449fb8abc3a6/torchvision-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917", size = 4325058, upload-time = "2026-01-21T16:27:26.165Z" }, - { url = "https://files.pythonhosted.org/packages/97/36/96374a4c7ab50dea9787ce987815614ccfe988a42e10ac1a2e3e5b60319a/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2", size = 1896207, upload-time = "2026-01-21T16:27:23.383Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e2/7abb10a867db79b226b41da419b63b69c0bd5b82438c4a4ed50e084c552f/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563", size = 2310741, upload-time = "2026-01-21T16:27:18.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/e6/0927784e6ffc340b6676befde1c60260bd51641c9c574b9298d791a9cda4/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443", size = 8089772, upload-time = "2026-01-21T16:27:14.048Z" }, - { url = "https://files.pythonhosted.org/packages/b6/37/e7ca4ec820d434c0f23f824eb29f0676a0c3e7a118f1514f5b949c3356da/torchvision-0.25.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977", size = 4425879, upload-time = "2026-01-21T16:27:12.579Z" }, + { url = "https://files.pythonhosted.org/packages/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" }, + { url = "https://files.pythonhosted.org/packages/da/80/0762f77f53605d10c9477be39bb47722cc8e383bbbc2531471ce0e396c07/torchvision-0.26.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5d63dd43162691258b1b3529b9041bac7d54caa37eae0925f997108268cbf7c4", size = 1860809, upload-time = "2026-03-23T18:12:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/e6/81/0b3e58d1478c660a5af4268713486b2df7203f35abd9195fea87348a5178/torchvision-0.26.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a39c7a26538c41fda453f9a9692b5ff9b35a5437db1d94f3027f6f509c160eac", size = 7727494, upload-time = "2026-03-23T18:12:46.062Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/d9ab5d29115aa05e12e30f1397a3eeae1d88a511241dc3bce48dc4342675/torchvision-0.26.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b7e6213620bbf97742e5f79832f9e9d769e6cf0f744c5b53dad80b76db633691", size = 7521747, upload-time = "2026-03-23T18:12:36.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1b/f1bc86a918c5f6feab1eeff11982e2060f4704332e96185463d27855bdf5/torchvision-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:4280c35ec8cba1fcc8294fb87e136924708726864c379e4c54494797d86bc474", size = 4319880, upload-time = "2026-03-23T18:12:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/66/28/b4ad0a723ed95b003454caffcc41894b34bd8379df340848cae2c33871de/torchvision-0.26.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:358fc4726d0c08615b6d83b3149854f11efb2a564ed1acb6fce882e151412d23", size = 1951973, upload-time = "2026-03-23T18:12:48.781Z" }, + { url = "https://files.pythonhosted.org/packages/71/e2/7a89096e6cf2f3336353b5338ba925e0addf9d8601920340e6bdf47e8eb3/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:3daf9cc149cf3cdcbd4df9c59dae69ffca86c6823250442c3bbfd63fc2e26c61", size = 7728679, upload-time = "2026-03-23T18:12:26.196Z" }, + { url = "https://files.pythonhosted.org/packages/69/1d/4e1eebc17d18ce080a11dcf3df3f8f717f0efdfa00983f06e8ba79259f61/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:82c3965eca27e86a316e31e4c3e5a16d353e0bcbe0ef8efa2e66502c54493c4b", size = 7609138, upload-time = "2026-03-23T18:12:35.327Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a4/f1155e943ae5b32400d7000adc81c79bb0392b16ceb33bcf13e02e48cced/torchvision-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ebc043cc5a4f0bf22e7680806dbba37ffb19e70f6953bbb44ed1a90aeb5c9bea", size = 4248202, upload-time = "2026-03-23T18:12:41.423Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c8/9bffa9c7f7bdf95b2a0a2dc535c290b9f1cc580c3fb3033ab1246ffffdeb/torchvision-0.26.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:eb61804eb9dbe88c5a2a6c4da8dec1d80d2d0a6f18c999c524e32266cb1ebcd3", size = 1860813, upload-time = "2026-03-23T18:12:39.636Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ac/48f28ffd227991f2e14f4392dde7e8dc14352bb9428c1ef4a4bbf5f7ed85/torchvision-0.26.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:9a904f2131cbfadab4df828088a9f66291ad33f49ff853872aed1f86848ef776", size = 7727777, upload-time = "2026-03-23T18:12:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/a4/21/a2266f7f1b0e58e624ff15fd6f01041f59182c49551ece0db9a183071329/torchvision-0.26.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0f3e572efe62ad645017ea847e0b5e4f2f638d4e39f05bc011d1eb9ac68d4806", size = 7522174, upload-time = "2026-03-23T18:12:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/1666f90bc0bdd77aaa11dcc42bb9f621a9c3668819c32430452e3d404730/torchvision-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:114bec0c0e98aa4ba446f63e2fe7a2cbca37b39ac933987ee4804f65de121800", size = 4348469, upload-time = "2026-03-23T18:12:24.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/1f0402ac55c2ae15651ff831957d083fe70b2d12282e72612a30ba601512/torchvision-0.26.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:b7d3e295624a28b3b1769228ce1345d94cf4d390dd31136766f76f2d20f718da", size = 1860826, upload-time = "2026-03-23T18:12:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6a/18a582fe3c5ee26f49b5c9fb21ad8016b4d1c06d10178894a58653946fda/torchvision-0.26.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7058c5878262937e876f20c25867b33724586aa4499e2853b2d52b99a5e51953", size = 7729089, upload-time = "2026-03-23T18:12:31.394Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/f7e119b59499edc00c55c03adc9ec3bd96144d9b81c46852c431f9c64a9a/torchvision-0.26.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8008474855623c6ba52876589dc52df0aa66e518c25eca841445348e5f79844c", size = 7522704, upload-time = "2026-03-23T18:12:20.301Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6a/09f3844c10643f6c0de5d95abc863420cfaf194c88c7dffd0ac523e2015f/torchvision-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e9d0e022c19a78552fb055d0414d47fecb4a649309b9968573daea160ba6869c", size = 4454275, upload-time = "2026-03-23T18:12:27.487Z" }, ] [[package]] @@ -6332,10 +6379,15 @@ name = "triton" version = "3.6.0" source = { registry = "https://pypi.org/simple" } wheels = [ + { 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" }, + { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, ] From 9e83510c99daeb0790f41df843820f1e381cac9c Mon Sep 17 00:00:00 2001 From: Jash Shah Date: Sun, 10 May 2026 08:30:37 -0700 Subject: [PATCH 05/18] fix(datasets): close file handle on VideoDecoder init failure in cache (#3542) If VideoDecoder() raises during initialization, the fsspec file handle was leaked since it was opened via __enter__() but never closed on the exception path. Now explicitly closes the handle before re-raising. --- src/lerobot/datasets/video_utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lerobot/datasets/video_utils.py b/src/lerobot/datasets/video_utils.py index 158e68cdb..512dc6d9b 100644 --- a/src/lerobot/datasets/video_utils.py +++ b/src/lerobot/datasets/video_utils.py @@ -282,7 +282,11 @@ class VideoDecoderCache: with self._lock: if video_path not in self._cache: file_handle = fsspec.open(video_path).__enter__() - decoder = VideoDecoder(file_handle, seek_mode="approximate") + try: + decoder = VideoDecoder(file_handle, seek_mode="approximate") + except Exception: + file_handle.close() + raise self._cache[video_path] = (decoder, file_handle) return self._cache[video_path][0] From b607c8458ec856dbe71a887f8cd16831dce4fc7c Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Mon, 11 May 2026 15:19:12 +0200 Subject: [PATCH 06/18] docs: add policy & compute guide (#3534) * docs(policy): contributing a policy guide * docs(training): HW compute guide * chore(docs): add to readme and index * Apply suggestions from code review Co-authored-by: Haoming Song <1847575517@qq.com> Signed-off-by: Steven Palma * chore(docs): slight improvements * refactor(docs): consolidate add policy docs * chore(style): fix pre-commit --------- Signed-off-by: Steven Palma Co-authored-by: Haoming Song <1847575517@qq.com> --- AGENT_GUIDE.md | 2 + README.md | 2 +- docs/source/_toctree.yml | 12 +- docs/source/bring_your_own_policies.mdx | 301 +++++++++++++++++------- docs/source/hardware_guide.mdx | 98 ++++++++ 5 files changed, 328 insertions(+), 87 deletions(-) create mode 100644 docs/source/hardware_guide.mdx diff --git a/AGENT_GUIDE.md b/AGENT_GUIDE.md index 725948dc9..57a33fdba 100644 --- a/AGENT_GUIDE.md +++ b/AGENT_GUIDE.md @@ -232,6 +232,8 @@ Match the policy to the user's **GPU memory** and **time budget**. Numbers below All policies typically train for **5–10 epochs** (see §7). +> **Human-facing version:** the [Compute Hardware Guide](./docs/source/hardware_guide.mdx) reuses the table below and adds a cloud-GPU tier guide and a Hugging Face Jobs pointer. + | Policy | Batch | Update (ms) | Peak GPU mem (GB) | Best for | | ----------- | ----: | ----------: | ----------------: | ------------------------------------------------------------------------------------------------ | | `act` | 4 | **83.9** | **0.94** | First-time users, laptops, single-task. Fast and reliable. | diff --git a/README.md b/README.md index afba8ff49..9c40e8b34 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ lerobot-train \ Similarly to the hardware, you can easily implement your own policy & leverage LeRobot's data collection, training, and visualization tools, and share your model to the HF Hub -For detailed policy setup guides, see the [Policy Documentation](https://huggingface.co/docs/lerobot/bring_your_own_policies). +For detailed policy setup guides, see the [Policy Documentation](https://huggingface.co/docs/lerobot/bring_your_own_policies). For GPU/RAM requirements and expected training time per policy, see the [Compute Hardware Guide](https://huggingface.co/docs/lerobot/hardware_guide). ## Inference & Evaluation diff --git a/docs/source/_toctree.yml b/docs/source/_toctree.yml index 40cec863f..87fcacf42 100644 --- a/docs/source/_toctree.yml +++ b/docs/source/_toctree.yml @@ -8,7 +8,7 @@ - local: il_robots title: Imitation Learning for Robots - local: bring_your_own_policies - title: Bring Your Own Policies + title: Adding a Policy - local: integrate_hardware title: Bring Your Own Hardware - local: hilserl @@ -24,6 +24,12 @@ - local: rename_map title: Using Rename Map and Empty Cameras title: "Tutorials" +- sections: + - local: hardware_guide + title: Compute Hardware Guide + - local: torch_accelerators + title: PyTorch accelerators + title: "Compute & Hardware" - sections: - local: lerobot-dataset-v3 title: Using LeRobotDataset @@ -142,10 +148,6 @@ - local: cameras title: Cameras title: "Sensors" -- sections: - - local: torch_accelerators - title: PyTorch accelerators - title: "Supported Hardware" - sections: - local: notebooks title: Notebooks diff --git a/docs/source/bring_your_own_policies.mdx b/docs/source/bring_your_own_policies.mdx index 57ecc2fb2..1b3871516 100644 --- a/docs/source/bring_your_own_policies.mdx +++ b/docs/source/bring_your_own_policies.mdx @@ -1,60 +1,37 @@ -# Bring Your Own Policies +# Adding a Policy -This tutorial explains how to integrate your own custom policy implementations into the LeRobot ecosystem, allowing you to leverage all LeRobot tools for training, evaluation, and deployment while using your own algorithms. +This guide walks you through implementing a custom policy and getting it to work with LeRobot's training, evaluation, and deployment tools. There are two paths: -## Step 1: Create a Policy Package +- **Plugin (out-of-tree)** — ship your policy as a standalone `lerobot_policy_*` package. Faster, no PR required, easy to iterate. Right for experimentation, internal use, or when you want to publish independently. +- **In-tree (contributed to LeRobot)** — land your policy directly in `src/lerobot/policies/`. Requires a PR, but makes your policy a first-class citizen of the library. -Your custom policy should be organized as an installable Python package following LeRobot's plugin conventions. +The plugin route is usually the right starting point — promote to in-tree once the policy has stabilized and there's clear value in shipping it with the library. -### Package Structure +Either way, the building blocks are the same: a configuration class, a policy class, and a processor factory. The first half of this guide covers those shared pieces; the second half covers the path-specific scaffolding ([Path A](#path-a-out-of-tree-plugin), [Path B](#path-b-contributing-in-tree)). -Create a package with the prefix `lerobot_policy_` (IMPORTANT!) followed by your policy name: +A note on tone: robot-learning is an actively evolving field, and "what a policy looks like" can shift with each new architecture. The conventions described here exist because they let `lerobot-train` and `lerobot-eval` work uniformly across very different models. When a new policy genuinely doesn't fit them, raise it (in your PR, or an issue) — the conventions are not sacred. -```bash -lerobot_policy_my_custom_policy/ -├── pyproject.toml -└── src/ - └── lerobot_policy_my_custom_policy/ - ├── __init__.py - ├── configuration_my_custom_policy.py - ├── modeling_my_custom_policy.py - └── processor_my_custom_policy.py -``` +--- -### Package Configuration +## Anatomy of a policy -Set up your `pyproject.toml`: +Three building blocks make up every policy. The names below use `my_policy` as a placeholder — replace with your policy's name. That name is load-bearing: it must match the string you pass to `@PreTrainedConfig.register_subclass`, the `MyPolicy.name` class attribute, and the `make__pre_post_processors` factory function (more on each below). -```toml -[project] -name = "lerobot_policy_my_custom_policy" -version = "0.1.0" -dependencies = [ - # your policy-specific dependencies -] -requires-python = ">= 3.12" +### Configuration class -[build-system] -build-backend = # your-build-backend -requires = # your-build-system -``` - -## Step 2: Define the Policy Configuration - -Create a configuration class that inherits from [`PreTrainedConfig`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/configs/policies.py) and registers your policy type: -Here is a template to get you started, customize the parameters and methods as needed for your policy's architecture and training requirements. +Inherit from [`PreTrainedConfig`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/configs/policies.py) and register your policy type. Here is a template — customize the parameters and methods as needed for your policy's architecture and training requirements. ```python -# configuration_my_custom_policy.py +# configuration_my_policy.py from dataclasses import dataclass, field from lerobot.configs import PreTrainedConfig from lerobot.optim import AdamWConfig from lerobot.optim import CosineDecayWithWarmupSchedulerConfig -@PreTrainedConfig.register_subclass("my_custom_policy") +@PreTrainedConfig.register_subclass("my_policy") @dataclass -class MyCustomPolicyConfig(PreTrainedConfig): - """Configuration class for MyCustomPolicy. +class MyPolicyConfig(PreTrainedConfig): + """Configuration class for MyPolicy. Args: n_obs_steps: Number of observation steps to use as input @@ -77,16 +54,20 @@ class MyCustomPolicyConfig(PreTrainedConfig): raise ValueError("n_action_steps cannot exceed horizon") def validate_features(self) -> None: - """Validate input/output feature compatibility.""" + """Validate input/output feature compatibility. + + Call this explicitly from your policy's __init__ — the base class does not. + """ if not self.image_features: - raise ValueError("MyCustomPolicy requires at least one image feature.") + raise ValueError("MyPolicy requires at least one image feature.") if self.action_feature is None: - raise ValueError("MyCustomPolicy requires 'action' in output_features.") + raise ValueError("MyPolicy requires 'action' in output_features.") def get_optimizer_preset(self) -> AdamWConfig: return AdamWConfig(lr=self.optimizer_lr, weight_decay=self.optimizer_weight_decay) def get_scheduler_preset(self): + """Return a LRSchedulerConfig from lerobot.optim, or None.""" return None @property @@ -101,8 +82,7 @@ class MyCustomPolicyConfig(PreTrainedConfig): @property def action_delta_indices(self) -> list[int]: - """Relative timestep offsets for the action chunk the dataset loader returns. - """ + """Relative timestep offsets for the action chunk the dataset loader returns.""" return list(range(self.horizon)) @property @@ -110,32 +90,34 @@ class MyCustomPolicyConfig(PreTrainedConfig): return None ``` -## Step 3: Implement the Policy Class +The string you pass to `@register_subclass` must match `MyPolicy.name` (next section) and is what users supply as `--policy.type` on the CLI. Default to `AdamW` from `lerobot.optim` for `get_optimizer_preset` unless you genuinely need otherwise. -Create your policy implementation by inheriting from [`PreTrainedPolicy`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/policies/pretrained.py): +### Policy class + +Inherit from [`PreTrainedPolicy`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/policies/pretrained.py) and set two class attributes — both are checked by `__init_subclass__`: ```python -# modeling_my_custom_policy.py +# modeling_my_policy.py import torch import torch.nn as nn from typing import Any from lerobot.policies import PreTrainedPolicy from lerobot.utils.constants import ACTION -from .configuration_my_custom_policy import MyCustomPolicyConfig +from .configuration_my_policy import MyPolicyConfig -class MyCustomPolicy(PreTrainedPolicy): - config_class = MyCustomPolicyConfig # must match the string in @register_subclass - name = "my_custom_policy" +class MyPolicy(PreTrainedPolicy): + config_class = MyPolicyConfig # must match the string in @register_subclass + name = "my_policy" - def __init__(self, config: MyCustomPolicyConfig, dataset_stats: dict[str, Any] = None): + def __init__(self, config: MyPolicyConfig, dataset_stats: dict[str, Any] = None): super().__init__(config, dataset_stats) config.validate_features() # not called automatically by the base class self.config = config self.model = ... # your nn.Module here def reset(self): - """Reset episode state.""" + """Reset per-episode state. Called by lerobot-eval at the start of each episode.""" ... def get_optim_params(self) -> dict: @@ -147,35 +129,51 @@ class MyCustomPolicy(PreTrainedPolicy): ... def select_action(self, batch: dict[str, torch.Tensor], **kwargs) -> torch.Tensor: - """Return a single action for the current timestep (called at inference).""" + """Return a single action for the current timestep (called every step at inference).""" ... - def forward(self, batch: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: + def forward(self, batch: dict[str, torch.Tensor]) -> tuple[torch.Tensor, dict | None]: """Compute the training loss. + Returns `(loss, output_dict)`. `output_dict` may be `None`; everything in it must be + logging-friendly Python natives (no tensors with gradients). + `batch["action_is_pad"]` is a bool mask of shape (B, horizon) that marks - timesteps padded because the episode ended before `horizon` steps, you + timesteps padded because the episode ended before `horizon` steps; you can exclude those from your loss. """ actions = batch[ACTION] action_is_pad = batch.get("action_is_pad") ... - return {"loss": ...} + return loss, {"some_loss_component": some_loss_component.item()} ``` -## Step 4: Add Data Processors +The methods called by the train/eval loops: -Create processor functions. For a concrete reference, see [processor_act.py](https://github.com/huggingface/lerobot/blob/main/src/lerobot/policies/act/processor_act.py) or [processor_diffusion.py](https://github.com/huggingface/lerobot/blob/main/src/lerobot/policies/diffusion/processor_diffusion.py). +| Method | Used by | What it does | +| ----------------------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `reset() -> None` | `lerobot-eval` | Clear per-episode state at the start of each episode. | +| `select_action(batch, **kwargs) -> Tensor` | `lerobot-eval` | Return the next action `(B, action_dim)`. Called every step. | +| `predict_action_chunk(batch, **kwargs) -> Tensor` | the policy itself | Return an action chunk `(B, chunk_size, action_dim)`. Currently abstract on the base class — raise `NotImplementedError` if your policy doesn't chunk. | +| `forward(batch, reduction="mean") -> tuple[Tensor, dict \| None]` | `lerobot-train` | Return `(loss, output_dict)`. Accept `reduction="none"` if you want to support per-sample weighting. | +| `get_optim_params() -> dict` | the optimizer | Return `self.parameters()` for simple policies; return a named parameter dict for [multi-optimizer policies](https://github.com/huggingface/lerobot/blob/ecd38c50d7d15b4184cf42649ff1185ee2e11eeb/src/lerobot/policies/sac/modeling_sac.py#L61-L73). | +| `update() -> None` _(optional)_ | `lerobot-train` | Called after each optimizer step _if defined_. Use for EMA, target nets, replay buffers (TDMPC uses this). | + +Batches are flat dictionaries keyed by the constants in [`lerobot.utils.constants`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/utils/constants.py): `OBS_STATE` (`observation.state.`), `OBS_IMAGES` (`observation.images.`), `OBS_LANGUAGE`, `ACTION`, etc. Reuse the constants — don't invent new prefixes. + +### Processor functions + +LeRobot uses `PolicyProcessorPipeline`s to normalize inputs and de-normalize outputs around your policy. For a concrete reference, see [`processor_act.py`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/policies/act/processor_act.py) or [`processor_diffusion.py`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/policies/diffusion/processor_diffusion.py). ```python -# processor_my_custom_policy.py +# processor_my_policy.py from typing import Any import torch from lerobot.processor import PolicyAction, PolicyProcessorPipeline -def make_my_custom_policy_pre_post_processors( +def make_my_policy_pre_post_processors( config, dataset_stats: dict[str, dict[str, torch.Tensor]] | None = None, ) -> tuple[ @@ -187,11 +185,48 @@ def make_my_custom_policy_pre_post_processors( return preprocessor, postprocessor ``` -**Important - function naming:** LeRobot discovers your processor by name. The function **must** be called `make_{policy_name}_pre_post_processors` (matching the string you passed to `@PreTrainedConfig.register_subclass`). +**Important — function naming:** LeRobot discovers your processor by name. The function **must** be called `make_{policy_name}_pre_post_processors` (matching the string you passed to `@PreTrainedConfig.register_subclass`). -## Step 5: Package Initialization +--- -Expose your classes in the package's `__init__.py`: +## Path A: Out-of-tree plugin + +The fastest way to ship a policy: package it as a standalone Python distribution and install it alongside LeRobot. No PR required, you own the release cycle, and you can publish to PyPI under your own namespace. + +### Package structure + +Create a package with the prefix `lerobot_policy_` (IMPORTANT!) followed by your policy name: + +```bash +lerobot_policy_my_policy/ +├── pyproject.toml +└── src/ + └── lerobot_policy_my_policy/ + ├── __init__.py + ├── configuration_my_policy.py + ├── modeling_my_policy.py + └── processor_my_policy.py +``` + +### `pyproject.toml` + +```toml +[project] +name = "lerobot_policy_my_policy" +version = "0.1.0" +dependencies = [ + # your policy-specific dependencies +] +requires-python = ">= 3.12" + +[build-system] +build-backend = # your-build-backend +requires = # your-build-system +``` + +### Package `__init__.py` + +Expose your classes in the package's `__init__.py` and guard against missing `lerobot`: ```python # __init__.py @@ -204,44 +239,148 @@ except ImportError: "lerobot is not installed. Please install lerobot to use this policy package." ) -from .configuration_my_custom_policy import MyCustomPolicyConfig -from .modeling_my_custom_policy import MyCustomPolicy -from .processor_my_custom_policy import make_my_custom_policy_pre_post_processors +from .configuration_my_policy import MyPolicyConfig +from .modeling_my_policy import MyPolicy +from .processor_my_policy import make_my_policy_pre_post_processors __all__ = [ - "MyCustomPolicyConfig", - "MyCustomPolicy", - "make_my_custom_policy_pre_post_processors", + "MyPolicyConfig", + "MyPolicy", + "make_my_policy_pre_post_processors", ] ``` -## Step 6: Installation and Usage - -### Install Your Policy Package +### Install and use ```bash -cd lerobot_policy_my_custom_policy +cd lerobot_policy_my_policy pip install -e . # Or install from PyPI if published -pip install lerobot_policy_my_custom_policy +pip install lerobot_policy_my_policy ``` -### Use Your Policy - Once installed, your policy automatically integrates with LeRobot's training and evaluation tools: ```bash lerobot-train \ - --policy.type my_custom_policy \ + --policy.type my_policy \ --env.type pusht \ --steps 200000 ``` -## Examples and Community Contributions +--- + +## Path B: Contributing in-tree + +When your policy has stabilized and there's clear value in shipping it with the library, you can land it directly in LeRobot. Read the general [contribution guide](./contributing) and the [PR template](https://github.com/huggingface/lerobot/blob/main/.github/PULL_REQUEST_TEMPLATE.md) first — that's where you'll find the testing/quality expectations every PR has to meet (`pre-commit run -a`, `pytest`, the community-review rule, etc.). What's below is the policy-specific layer on top of that. + +### In-tree layout + +``` +src/lerobot/policies/my_policy/ +├── __init__.py # re-exports config + modeling + processor factory +├── configuration_my_policy.py # MyPolicyConfig + @register_subclass +├── modeling_my_policy.py # MyPolicy(PreTrainedPolicy) +├── processor_my_policy.py # make_my_policy_pre_post_processors +└── README.md # symlink → ../../../../docs/source/policy_my_policy_README.md +``` + +Two notes: + +- The `README.md` next to the source is a **symlink** into `docs/source/policy__README.md` — the actual file lives under `docs/`. Existing policies (act, smolvla, diffusion, …) all do this; copy one of those symlinks. The policy README is conventionally minimal: paper link + BibTeX citation. +- The user-facing tutorial — what to install, how to train, hyperparameters, benchmark numbers — lives separately at `docs/source/.mdx` and is registered in `_toctree.yml` under "Policies". + +The file names are load-bearing: the factory does lazy imports by name, and the processor is discovered by the `make__pre_post_processors` convention. + +### Wiring + +Three places need to know about your policy. All by name. + +1. **`policies/__init__.py`** — re-export `MyPolicyConfig` and add it to `__all__`. **Don't** re-export the modeling class; it loads lazily through the factory (so `import lerobot` stays fast). +2. **`factory.py:get_policy_class`** — add a branch returning `MyPolicy` from a lazy import. +3. **`factory.py:make_policy_config`** and **`factory.py:make_pre_post_processors`** — same idea, two more branches. + +Mirror an existing policy that's structurally similar to yours; the diff is small. + +### Heavy / optional dependencies + +Most policies need a heavy backbone (transformers, diffusers, a specific VLM SDK). The convention is **two-step gating**: a `TYPE_CHECKING`-guarded import at module top, and a `require_package` runtime check in the constructor. [`modeling_diffusion.py`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/policies/diffusion/modeling_diffusion.py) is the canonical reference: + +```python +from typing import TYPE_CHECKING +from lerobot.utils.import_utils import _diffusers_available, require_package + +if TYPE_CHECKING or _diffusers_available: + from diffusers.schedulers.scheduling_ddim import DDIMScheduler +else: + DDIMScheduler = None # keeps the symbol bindable at import time + +class DiffusionPolicy(PreTrainedPolicy): + def __init__(self, config): + require_package("diffusers", extra="diffusion") + super().__init__(config) + ... +``` + +This way: + +- `import lerobot.policies` keeps working without the extra installed (the symbol is just bound to `None`). +- Type checkers see the real symbol. +- Instantiating the policy without the extra raises a clear `ImportError` pointing at `pip install 'lerobot[diffusion]'`. + +Add a matching extra to [`pyproject.toml`](https://github.com/huggingface/lerobot/blob/main/pyproject.toml) `[project.optional-dependencies]` and include it in the `all` extra so `pip install 'lerobot[all]'` keeps installing everything. + +### Benchmarks and a published checkpoint + +A new policy is much easier to review — and far more useful — when it ships with a working checkpoint and at least one number you can reproduce. + +**Pick at least one in-tree benchmark.** LeRobot ships sim benchmarks with per-benchmark Docker images (LIBERO, LIBERO-plus, Meta-World, RoboTwin 2.0, RoboCasa365, RoboCerebra, RoboMME, VLABench and more). Pick the one that matches your policy's modality — VLAs usually go to LIBERO or VLABench; image-only BC to LIBERO or Meta-World. The full list lives under [Benchmarks](./libero) in the docs sidebar. + +**Push the checkpoint & processors** to the Hub under `lerobot/_` (or your namespace if you don't have write access; a maintainer can mirror it). Use `PreTrainedPolicy.push_model_to_hub` so the repo gets `config.json`, `model.safetensors`, and a model card. + +**Report results in your policy's MDX**, with the exact `lerobot-eval` command and hardware so anyone can re-run: + +```markdown +## Results + +Evaluated on LIBERO with `lerobot/_libero`: + +| Suite | Success rate | n_episodes | +| -------------- | -----------: | ---------: | +| libero_spatial | 87.5% | 50 | +| libero_object | 93.0% | 50 | +| libero_goal | 81.5% | 50 | +| libero_10 | 62.0% | 50 | +| **average** | **81.0%** | 200 | + +Reproduce: `lerobot-eval --policy.path=lerobot/_libero --env.type=libero --env.task=libero_spatial --eval.n_episodes=50` (1× A100 40 GB). +``` + +Use `n_episodes ≥ 50` per suite for stable success-rate estimates. + +If your policy is real-robot-only and no sim benchmark applies, swap the sim eval for: a public training dataset on the Hub, the `lerobot-train` command, the checkpoint, and a real-robot success rate over ≥10 episodes via `lerobot-rollout --policy.path=...`. + +### PR checklist + +The general expectations are in [`CONTRIBUTING.md`](https://github.com/huggingface/lerobot/blob/main/CONTRIBUTING.md) and the [PR template](https://github.com/huggingface/lerobot/blob/main/.github/PULL_REQUEST_TEMPLATE.md). On top of those, reviewers will look for: + +- [ ] `MyPolicy` and `MyPolicyConfig` cover the surface above; `__init_subclass__` accepts the class. +- [ ] `factory.py` and `policies/__init__.py` are wired (lazy imports for modeling). +- [ ] `make_my_policy_pre_post_processors` follows the naming convention. +- [ ] Optional deps live behind a `[project.optional-dependencies]` extra and the `TYPE_CHECKING + require_package` guard. +- [ ] `tests/policies/` updated; backward-compat artifact committed & policy-specific tests. +- [ ] `src/lerobot/policies//README.md` symlinked into `docs/source/policy__README.md`; user-facing `docs/source/.mdx` written and added to `_toctree.yml`. +- [ ] At least one reproducible benchmark eval in the policy MDX with a published checkpoint (sim benchmark, or real-robot dataset + checkpoint). + +The fastest way to get a clean PR is to copy the directory of the existing policy closest to yours, rename, and replace contents method by method. Don't wait until everything is polished — open a draft PR early and iterate with us; reviewers would much rather give feedback on a half-finished branch than a fully-merged one. + +--- + +## Examples and community contributions Check out these example policy implementations: -- [DiTFlow Policy](https://github.com/danielsanjosepro/lerobot_policy_ditflow) - Diffusion Transformer policy with flow-matching objective. Try it out in this example: [DiTFlow Example](https://github.com/danielsanjosepro/test_lerobot_policy_ditflow) +- [DiTFlow Policy](https://github.com/danielsanjosepro/lerobot_policy_ditflow) — Diffusion Transformer policy with flow-matching objective. Try it out in this example: [DiTFlow Example](https://github.com/danielsanjosepro/test_lerobot_policy_ditflow) -Share your policy implementations with the community! 🤗 +Thanks for taking the time to bring a new policy into LeRobot. Every architecture that lands in `main` — and every plugin published by the community — makes the library a little more useful for the next person, and a little more representative of where robot learning is going. We're looking forward to seeing what you ship. 🤗 diff --git a/docs/source/hardware_guide.mdx b/docs/source/hardware_guide.mdx new file mode 100644 index 000000000..0998344ec --- /dev/null +++ b/docs/source/hardware_guide.mdx @@ -0,0 +1,98 @@ +# Compute HW Guide for LeRobot Training + +Rough sizing for training a LeRobot policy: how much VRAM each policy needs, what training time looks like, and where to run when local hardware isn't enough. + +The numbers below are **indicative** — order-of-magnitude figures for picking hardware, not exact predictions. Throughput depends heavily on dataset I/O, image resolution, batch size, and number of GPUs. + +## Memory by policy group + +Policies cluster by backbone size; the groupings below give a single VRAM envelope per group instead of repeating numbers per policy. Memory scales roughly linearly with batch size; AdamW (the LeRobot default) carries optimizer state that adds ~30–100% over a forward+backward pass alone. + +| Group | Policies | Peak VRAM (BS 8, AdamW) | Suitable starter GPUs | +| ---------- | ------------------------------------------- | ----------------------: | --------------------------------- | +| Light BC | `act`, `vqbet`, `tdmpc` | ~2–6GB | Laptop GPU (RTX 3060), L4, A10G | +| Diffusion | `diffusion`, `multi_task_dit` | ~8–14GB | RTX 4070+ / L4 / A10G | +| Small VLA | `smolvla` | ~10–16GB | RTX 4080+ / L4 / A10G | +| Large VLA | `pi0`, `pi0_fast`, `pi05`, `xvla`, `wall_x` | ~24–40GB | A100 40 GB+ (24 GB tight at BS 1) | +| Multimodal | `groot`, `eo1` | ~24–40GB | A100 40 GB+ | +| RL | `sac` | config-dep. | See [HIL-SERL guide](./hilserl) | + +Memory-bound? Drop the batch size (~linear), use gradient accumulation to recover effective batch, or for SmolVLA leave `freeze_vision_encoder=True`. + +## Training time + +Robotics imitation learning typically converges in **5–10 epochs over the dataset**, not hundreds of thousands of raw steps. Once you know your epoch count, wall-clock is essentially: + +```text +total_frames = sum of frames over all episodes # 50 ep × 30 fps × 30 s ≈ 45,000 +steps_per_epoch = ceil(total_frames / (num_gpus × batch_size)) +total_steps = epochs × steps_per_epoch +wall_clock ≈ total_steps × per_step_time +``` + +Per-step time depends on the policy and the GPU. The numbers in the table below are anchors — pick the row closest to your setup and scale linearly with `total_steps` if you train longer or shorter. + +### Common scenarios + +Indicative wall-clock for **5 epochs on a ~50-episode dataset (~45k frames at 30 fps × 30 s)**, default optimizer (AdamW), 640×480 images: + +| Setup | Policy | Batch | Wall-clock | +| ------------------------------------ | -------------- | ----- | ---------: | +| Single RTX 4090 / RTX 3090 (24 GB) | `act` | 8 | ~30–60min | +| Single RTX 4090 / RTX 3090 (24 GB) | `diffusion` | 8 | ~2–4h | +| Single L4 / A10G (24 GB) | `act` | 8 | ~1–2h | +| Single L4 / A10G (24 GB) | `smolvla` | 4 | ~3–6h | +| Single A100 40 GB | `smolvla` | 16 | ~1–2h | +| Single A100 40 GB | `pi0` / `pi05` | 4 | ~4–8h | +| 4× H100 80 GB cluster (`accelerate`) | `diffusion` | 32 | ~30–60min | +| 4× H100 80 GB cluster (`accelerate`) | `smolvla` | 32 | ~1–2h | +| Apple Silicon M1/M2/M3 Max (MPS) | `act` | 4 | ~6–14h | + +These are order-of-magnitude figures. Real runs deviate by ±50% depending on image resolution, dataset I/O, dataloader threading, and exact GPU SKU. They are useful as "is this run going to take an hour or a day?" intuition, not as SLAs. + +### Multi-GPU matters a lot + +`accelerate launch --num_processes=N` is the easiest way to cut training time. Each optimizer step processes `N × batch_size` samples in roughly the same wall-clock as a single-GPU step, so 4 GPUs ≈ 4× speedup for compute-bound runs. See the [Multi GPU training](./multi_gpu_training) guide for the full setup. + +Reference data points on a 4×H100 80 GB cluster (`accelerate launch --num_processes=4`), 5000 steps, batch 32, AdamW, dataset [`imstevenpmwork/super_poulain_draft`](https://huggingface.co/datasets/imstevenpmwork/super_poulain_draft) (~50 episodes, ~640×480 images): + +| Policy | Wall-clock | `update_s` | `dataloading_s` | GPU util | Notable flags | +| ----------- | ---------- | ---------: | --------------: | -------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `diffusion` | 16m 17s | 0.167 | 0.015 | ~90% | defaults (training from scratch) | +| `smolvla` | 27m 49s | 0.312 | 0.011 | ~80% | `--policy.path=lerobot/smolvla_base`, `freeze_vision_encoder=false`, `train_expert_only=false` | +| `pi05` | 3h 41m | 2.548 | 0.014 | ~95% | `--policy.pretrained_path=lerobot/pi05_base`, `gradient_checkpointing=true`, `dtype=bfloat16`, vision encoder + expert trained | + +The `dataloading_s` vs. `update_s` ratio is the diagnostic that matters: when `dataloading_s` approaches `update_s`, more GPUs stop helping — your dataloader is the bottleneck and you should look at `--num_workers`, image resolution, and disk speed before adding compute. + +### Schedule and checkpoints + +If you shorten training (e.g. 5k–10k steps on a small dataset), also shorten the LR schedule with `--policy.scheduler_decay_steps≈--steps`. Otherwise the LR stays near its peak and never decays. Same for `--save_freq`. + +## Where to run + +VRAM is the first filter. Within a tier, pick by budget and availability — the `$`–`$$$$` columns are relative; check current pricing on the provider you actually use. + +| Class | VRAM | Tier | Comfortable for | +| -------------------------- | ----- | ------ | ----------------------------------------------------------- | +| RTX 3090 / 4090 (consumer) | 24 GB | `$` | Light BC, Diffusion, SmolVLA. Tight for VLAs at batch 1. | +| L4 / A10G (cloud) | 24 GB | `$–$$` | Same envelope; common on Google Cloud, RunPod, AWS `g5/g6`. | +| A100 40 GB | 40 GB | `$$$` | Any policy at reasonable batch sizes. | +| A100 80 GB / H100 80 GB | 80 GB | `$$$$` | Multi-GPU clusters; large batches for VLAs. | +| **CPU only** | — | — | Don't train. Use Colab or rent a GPU. | + +### Hugging Face Jobs + +[Hugging Face Jobs](https://huggingface.co/docs/hub/jobs) lets you run training on managed HF infrastructure, billed by the second. The repo publishes a ready-to-use image: **`huggingface/lerobot-gpu:latest`**, rebuilt **every night at 02:00 UTC from `main`** ([`docker_publish.yml`](https://github.com/huggingface/lerobot/blob/main/.github/workflows/docker_publish.yml)) — so it tracks the current state of the repo, not a tagged release. + +```bash +hf jobs run --flavor a10g-large huggingface/lerobot-gpu:latest \ + bash -c "nvidia-smi && lerobot-train \ + --policy.type=act --dataset.repo_id=/ \ + --policy.repo_id=/act_ --batch_size=8 --steps=50000" +``` + +Notes: + +- The leading `nvidia-smi` is a quick sanity check that CUDA is visible inside the container — useful to fail fast if the flavor or driver mismatched. +- The default Job timeout is 30 minutes; pass `--timeout 4h` (or longer) for real training. +- `--flavor` maps onto the table above: `t4-small`/`t4-medium` (T4, ACT only), `l4x1`/`l4x4` (L4 24 GB), `a10g-small/large/largex2/largex4` (A10G 24 GB scaled out), `a100-large` (A100). For the current full catalogue + pricing see [https://huggingface.co/docs/hub/jobs](https://huggingface.co/docs/hub/jobs). From 6d269b28c8024ad7550d4be81ece00e6bb6396db Mon Sep 17 00:00:00 2001 From: Maxime Ellerbach Date: Mon, 11 May 2026 15:36:32 +0200 Subject: [PATCH 07/18] docs(omx): adding some examples and scripts (#3566) * docs(omx): adding some examples and scripts * cleaning up and reviewing the cli args * adding __init__.py to example folder, adjusting the examples * adding reference to pretrained act policy * moving `.send_action` before `dataset.add_frame` for consistency Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Maxime Ellerbach * adjusting docstring Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Maxime Ellerbach * adressing hardcoded dataset fps * removed init as it worked without --------- Signed-off-by: Maxime Ellerbach --- examples/omx/README.md | 136 ++++++++++ examples/omx/record_grab.py | 422 ++++++++++++++++++++++++++++++ examples/omx/reset_environment.py | 267 +++++++++++++++++++ 3 files changed, 825 insertions(+) create mode 100644 examples/omx/README.md create mode 100644 examples/omx/record_grab.py create mode 100644 examples/omx/reset_environment.py diff --git a/examples/omx/README.md b/examples/omx/README.md new file mode 100644 index 000000000..68aa3ee0d --- /dev/null +++ b/examples/omx/README.md @@ -0,0 +1,136 @@ +# OMX Follower — Cube Pick And Place Example + +This is an example of what is possible to do with LeRobot on a physical setup. +It is a WIP and being used internally at LeRobot and specific to our setup, but we hope it can be a useful reference for how to use LeRobot APIs and CLIs. + +It includes an end-to-end example for the **OMX Follower** robot arm: pick and place a cube dataset, train a policy, and deploy it autonomously. + +## Hardware + +| Component | Value | +| --------- | ------------------------------------ | +| Robot | OMX Follower | +| Cameras | 2× OpenCV cameras (wrist + top-down) | + +## Scripts + +| Script | Purpose | +| ---------------------- | --------------------------------------------------------------- | +| `reset_environment.py` | Standalone utility: sweep workspace, grab cube, place cube | +| `record_grab.py` | Automated data collection: reset → place → record grab episodes | + +## Setup + +Make sure you have LeRobot installed in your env. (See [the installation guide](https://huggingface.co/docs/lerobot/installation)) + +Next, we will declare some environment variables for convenience. Adjust the camera indices and robot port to match your system configuration. + +```bash +export ROBOT_PORT=/dev/ttyACM0 +export TELEOP_PORT=/dev/ttyACM1 +export HF_USERNAME= +export ROBOT_CAMERAS="{ wrist: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30, fourcc: MJPG}, top: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30, fourcc: MJPG} }" +``` + +## Step 1 — Collect Data + +```bash +lerobot-record \ + --robot.type=omx_follower \ + --robot.port=$ROBOT_PORT \ + --robot.id=omx_follower \ + --robot.cameras="$ROBOT_CAMERAS" \ + --teleop.type=omx_leader \ + --teleop.port=$TELEOP_PORT \ + --teleop.id=omx_leader \ + --dataset.repo_id=$HF_USERNAME/omx_pickandplace \ + --dataset.root=data/omx_pickandplace \ + --dataset.num_episodes=50 \ + --dataset.single_task="Pick the cube and place it in the blue square" \ + --dataset.streaming_encoding=true \ + --dataset.push_to_hub=true +``` + +### Bonus Auto-Collect script + +/!\ This is specific to our setup and the task of picking and placing a cube. It is not a general-purpose data collection script. As you may notice, it doesn't require a teleop. + +```bash +python -m examples.omx.record_grab \ + --robot.type=omx_follower \ + --robot.port=$ROBOT_PORT \ + --robot.id=omx_follower \ + --robot.cameras="$ROBOT_CAMERAS" \ + --dataset.repo_id=$HF_USERNAME/omx_pickandplace \ + --dataset.root=data/omx_pickandplace \ + --dataset.num_episodes=50 \ + --dataset.single_task="Pick the cube and place it in the blue square" \ + --dataset.streaming_encoding=true \ + --dataset.push_to_hub=true +``` + +Each episode: + +1. The arm grabs the cube from the center of the workspace and places it at a random position. +2. The arm returns to HOME. +3. A targeted grab is recorded: HOME → approach raised → lower onto cube → grasp → lift → carry → drop → HOME. + +A dataset is already available here [`maximellerbach/omx_pickandplace`](https://huggingface.co/datasets/maximellerbach/omx_pickandplace), so you can skip directly to training if you want. + +## Step 2 — Train + +To train a simple `ACT` policy on the collected dataset, you can use the `lerobot-train` CLI: + +```bash +lerobot-train \ + --dataset.repo_id=$HF_USERNAME/omx_pickandplace \ + --policy.type=act \ + --output_dir=outputs/train/omx_pickandplace_act \ + --policy.device=cuda \ + --policy.repo_id=$HF_USERNAME/omx_pickandplace_act \ + --steps=20000 \ + --wandb.enable=true +``` + +A pretrained `ACT` policy is already available here [`maximellerbach/omx_pickandplace_act`](https://huggingface.co/maximellerbach/omx_pickandplace_act). + +## Step 3 — Rollout + +Use the `lerobot-rollout` CLI with base strategy: + +```bash +lerobot-rollout \ + --strategy.type=base \ + --robot.type=omx_follower \ + --robot.port=$ROBOT_PORT \ + --robot.id=omx_follower \ + --robot.cameras="$ROBOT_CAMERAS" \ + --policy.path=$HF_USERNAME/omx_pickandplace_act \ +``` + +For continuous recording with automatic upload (sentry mode): + +```bash +lerobot-rollout \ + --strategy.type=sentry \ + --strategy.upload_every_n_episodes=10 \ + --robot.type=omx_follower \ + --robot.port=$ROBOT_PORT \ + --robot.id=omx_follower \ + --robot.cameras="$ROBOT_CAMERAS" \ + --policy.path=$HF_USERNAME/omx_pickandplace_act \ + --dataset.repo_id=$HF_USERNAME/rollout_omx_pickandplace_act \ +``` + +## Environment Reset Utility + +Those are specific to this particular physical setup. Those are scripts that execute hardcoded sequences of actions on the robot to reset the environment, which is useful for data collection and evaluation. They are not general-purpose scripts. + +`reset_environment.py` can be run standalone to prepare the workspace: + +```bash +# Grab cube + place it at a random position on the left side +python -m examples.omx.reset_environment --port $ROBOT_PORT --mode grab_and_place +``` + +It also exposes `grab_cube(robot)` and `place_cube(robot)` for use in custom scripts. diff --git a/examples/omx/record_grab.py b/examples/omx/record_grab.py new file mode 100644 index 000000000..bd3dfdf3b --- /dev/null +++ b/examples/omx/record_grab.py @@ -0,0 +1,422 @@ +#!/usr/bin/env python3 +""" +Auto-record grab episodes for the OMX robot arm. + +Each episode cycle: + 1. grab_and_place — grab cube from workspace center and place at a random (pan, reach) position + 2. HOME — return arm to home with gripper open + 3. record_grab — execute a targeted grab to the stored position while recording + observations + actions to a LeRobotDataset + +Usage (run from repo root): + python -m examples.omx.record_grab \\ + --robot.type=omx_follower \\ + --robot.port=/dev/ttyACM0 \\ + --robot.id=omx_follower \\ + --robot.cameras="{ wrist: {type: opencv, index_or_path: 6, width: 640, height: 480, fps: 30, fourcc: MJPG}, top: {type: opencv, index_or_path: 4, width: 640, height: 480, fps: 30, fourcc: MJPG} }" \\ + --dataset.repo_id=/ \\ + --dataset.root=data/omx_grab \\ + --dataset.num_episodes=50 \\ + --dataset.single_task="Grab the cube" \\ + --dataset.streaming_encoding=true +""" + +import logging +from dataclasses import dataclass +from pprint import pformat + +import numpy as np + +from lerobot.cameras import CameraConfig # noqa: F401 +from lerobot.cameras.opencv import OpenCVCameraConfig # noqa: F401 +from lerobot.configs import parser +from lerobot.configs.dataset import DatasetRecordConfig +from lerobot.datasets import ( + LeRobotDataset, + VideoEncodingManager, + aggregate_pipeline_dataset_features, + create_initial_features, +) +from lerobot.processor import make_default_processors +from lerobot.robots import RobotConfig, make_robot_from_config +from lerobot.robots.omx_follower import OmxFollower +from lerobot.utils.constants import ACTION, OBS_STR +from lerobot.utils.feature_utils import build_dataset_frame, combine_feature_dicts +from lerobot.utils.robot_utils import precise_sleep + +from .reset_environment import ( + APPROACH_SPEED, + GRIPPER_CLOSE_POS, + HOME_POSE, + PUSH_END_ELBOW_FLEX, + PUSH_END_SHOULDER_LIFT, + PUSH_START_ELBOW_FLEX, + PUSH_START_SHOULDER_LIFT, + array_to_pose, + grab_cube, + horizontal_wrist_flex, + move_to_pose, + place_cube, + pose_to_array, +) + +# ── Grab-episode motion parameters ──────────────────────────────────────────── + +# Shoulder-lift offset for the raised approach phase (subtracted from the target sl, arm is higher). +GRAB_RAISE_SL_OFFSET = 20.0 +GRAB_LOWER_SPEED = 20.0 +RECORD_SPEED = 30.0 + +# Pose the arm travels to after closing the gripper (cube held). +GRAB_CARRY_POSE = { + "shoulder_pan.pos": -23.0, + "shoulder_lift.pos": 5.0, + "elbow_flex.pos": 18.0, + "wrist_flex.pos": -14.0, + "wrist_roll.pos": 0.0, + "gripper.pos": GRIPPER_CLOSE_POS, +} + +# Per-joint jitter limits (degrees) applied to transit waypoints for human-like variation. +# Cube-approach and carry poses are never jittered to preserve precision. +_JITTER_LIMITS: dict[str, float] = { + "shoulder_pan.pos": 5.0, + "shoulder_lift.pos": 4.0, + "elbow_flex.pos": 4.0, + "wrist_flex.pos": 3.0, + "wrist_roll.pos": 2.0, + "gripper.pos": 0.0, +} + + +def _jitter_pose(pose: dict, rng: np.random.Generator) -> dict: + """Return a copy of pose with independent per-joint random perturbations.""" + return { + k: v + rng.uniform(-_JITTER_LIMITS.get(k, 0.0), _JITTER_LIMITS.get(k, 0.0)) for k, v in pose.items() + } + + +def _random_stuck_pose(rng: np.random.Generator) -> dict: + """Return a physically plausible stuck pose (failed grasp), gripper closed. + + ef bounds are piecewise-linear in sl so the arm stays in a reachable, + table-safe envelope across the full sl range: + sl=-50 → ef ∈ [ 0, 50] (arm raised, can be bent forward) + sl= 0 → ef ∈ [-25, 25] (mid reach) + sl= 30 → ef ∈ [-20, 0] (arm extended, little room to flex) + wrist_flex is randomly offset from the horizontal value. + """ + pan = float(rng.uniform(-5.0, 35.0)) + sl = float(rng.uniform(-50.0, 30.0)) + + if sl <= 0.0: + alpha = (sl + 50.0) / 50.0 # 0 at sl=-50, 1 at sl=0 + ef_lo = alpha * -25.0 # 0 → -25 + ef_hi = 50.0 + alpha * -25.0 # 50 → 25 + else: + alpha = sl / 30.0 # 0 at sl=0, 1 at sl=30 + ef_lo = -25.0 + alpha * 5.0 # -25 → -20 + ef_hi = 25.0 + alpha * -25.0 # 25 → 0 + + ef = float(rng.uniform(ef_lo, ef_hi)) + wf = horizontal_wrist_flex(sl, ef) + float(rng.uniform(-15.0, 15.0)) + return { + "shoulder_pan.pos": pan, + "shoulder_lift.pos": sl, + "elbow_flex.pos": ef, + "wrist_flex.pos": wf, + "wrist_roll.pos": float(rng.uniform(-15.0, 15.0)), + "gripper.pos": GRIPPER_CLOSE_POS, + } + + +logger = logging.getLogger(__name__) + + +@dataclass +class OmxRecordGrabConfig: + robot: RobotConfig + dataset: DatasetRecordConfig + # Resume recording on an existing dataset. + resume: bool = False + # Fraction of episodes that start from a random stuck pose (gripper closed) to + # generate recovery data. 0.0 = disabled, 1.0 = all episodes are recovery starts. + recovery_prob: float = 0.5 + + +def record_episode_spline( + robot: OmxFollower, + waypoints: list[dict], + speeds: list[float], + dataset: LeRobotDataset, + task: str, +) -> None: + """Execute a Catmull-Rom-style spline through waypoints, recording each frame. + + Segment durations are parameterized from the maximum absolute joint delta + between consecutive waypoints divided by the requested segment speed, + producing non-uniform timing in joint space. Interior tangents are derived + from the adjacent per-segment velocities, with clamped (zero-velocity) + endpoints so the arm starts and stops smoothly. Each segment is cubic + Hermite, giving C1 continuity at every waypoint. + """ + pts = [pose_to_array(w) for w in waypoints] + n = len(pts) + + # Steps and duration per segment + n_steps_list = [] + timestamps = [] + for i in range(n - 1): + max_dist = float(np.max(np.abs(pts[i + 1] - pts[i]))) + ns = max(1, int(max_dist / speeds[i] * dataset.fps)) if max_dist >= 0.5 else 0 + n_steps_list.append(ns) + timestamps.append(ns / dataset.fps) + + # Velocity tangents (deg/sec) — clamped at endpoints, Catmull-Rom for interior + vels = [np.zeros_like(pts[0])] + for i in range(1, n - 1): + v_prev = (pts[i] - pts[i - 1]) / timestamps[i - 1] if timestamps[i - 1] > 0 else np.zeros_like(pts[0]) + v_next = (pts[i + 1] - pts[i]) / timestamps[i] if timestamps[i] > 0 else np.zeros_like(pts[0]) + vels.append(0.5 * (v_prev + v_next)) + vels.append(np.zeros_like(pts[0])) + + dt = 1.0 / dataset.fps + for seg in range(n - 1): + ns = n_steps_list[seg] + if ns == 0: + continue + p0, p1 = pts[seg], pts[seg + 1] + # Scale velocity (deg/sec) to t-space tangent (deg/t-unit, where t: 0→1 over ns steps) + m0 = vels[seg] * timestamps[seg] + m1 = vels[seg + 1] * timestamps[seg] + + for step in range(1, ns + 1): + t = step / ns + h00 = 2 * t**3 - 3 * t**2 + 1 + h10 = t**3 - 2 * t**2 + t + h01 = -2 * t**3 + 3 * t**2 + h11 = t**3 - t**2 + commanded = h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1 + + action = array_to_pose(commanded) + robot.send_action(action) + obs = robot.get_observation() + obs_frame = build_dataset_frame(dataset.features, obs, prefix=OBS_STR) + action_frame = build_dataset_frame(dataset.features, action, prefix=ACTION) + dataset.add_frame({**obs_frame, **action_frame, "task": task}) + precise_sleep(dt) + + +def record_grab_episode( + robot: OmxFollower, + dataset: LeRobotDataset, + pan: float, + t: float, + task: str, + recovery_start: bool = False, +) -> None: + """Execute a targeted grab to the stored (pan, t) position, recording every frame. + + Normal sequence (initial HOME move is NOT recorded): + HOME → raised approach above cube → lower → close gripper + → raise [jittered] → retract [jittered] → GRAB_CARRY_POSE → drop → HOME + + Recovery sequence (recovery_start=True): arm is moved to a random stuck pose + (gripper closed) without recording, then recording begins from there: + stuck_pose → raised approach above cube → [normal grab sequence from there] + + All segments are joined by a Catmull-Rom spline (C1-continuous velocities). + """ + sl = PUSH_START_SHOULDER_LIFT + t * (PUSH_END_SHOULDER_LIFT - PUSH_START_SHOULDER_LIFT) + ef = PUSH_START_ELBOW_FLEX + t * (PUSH_END_ELBOW_FLEX - PUSH_START_ELBOW_FLEX) + sl_raised = sl - GRAB_RAISE_SL_OFFSET + wf_horizontal = horizontal_wrist_flex(sl, ef) + + rng = np.random.default_rng() + + if recovery_start: + stuck_pose = _random_stuck_pose(rng) + logger.info(f"Recovery start: {stuck_pose}") + move_to_pose(robot, stuck_pose, APPROACH_SPEED) + first_waypoints = [stuck_pose] + first_speeds = [] + else: + jittery_start = _jitter_pose(HOME_POSE, rng) + move_to_pose(robot, jittery_start, APPROACH_SPEED) + first_waypoints = [jittery_start] + first_speeds = [] + + waypoints = first_waypoints + [ + { # raised approach: arm above cube + "shoulder_pan.pos": pan, + "shoulder_lift.pos": sl_raised, + "elbow_flex.pos": ef, + "wrist_flex.pos": horizontal_wrist_flex(sl_raised, ef), + "wrist_roll.pos": 0.0, + "gripper.pos": 60.0, + }, + { # lower onto cube — no jitter: precision needed + "shoulder_pan.pos": pan, + "shoulder_lift.pos": sl, + "elbow_flex.pos": ef, + "wrist_flex.pos": wf_horizontal, + "wrist_roll.pos": 0.0, + "gripper.pos": 60.0, + }, + { # close gripper — no jitter: precision needed + "shoulder_pan.pos": pan, + "shoulder_lift.pos": sl, + "elbow_flex.pos": ef, + "wrist_flex.pos": wf_horizontal, + "wrist_roll.pos": 0.0, + "gripper.pos": GRIPPER_CLOSE_POS, + }, + _jitter_pose( + { # raise with cube + "shoulder_pan.pos": pan, + "shoulder_lift.pos": sl_raised, + "elbow_flex.pos": ef, + "wrist_flex.pos": horizontal_wrist_flex(sl_raised, ef), + "wrist_roll.pos": 0.0, + "gripper.pos": GRIPPER_CLOSE_POS, + }, + rng, + ), + _jitter_pose( + { # retract: fold arm toward HOME before sweeping to carry zone + "shoulder_pan.pos": pan * 0.25, + "shoulder_lift.pos": HOME_POSE["shoulder_lift.pos"] + 5.0, + "elbow_flex.pos": HOME_POSE["elbow_flex.pos"] - 5.0, + "wrist_flex.pos": 0.0, + "wrist_roll.pos": 0.0, + "gripper.pos": GRIPPER_CLOSE_POS, + }, + rng, + ), + GRAB_CARRY_POSE, # no jitter: target drop zone + {**GRAB_CARRY_POSE, "gripper.pos": 60.0}, # drop cube + HOME_POSE, + ] + speeds = first_speeds + [ + RECORD_SPEED, # (HOME →) raised approach + GRAB_LOWER_SPEED, # raised approach → lower + GRAB_LOWER_SPEED, # lower → close gripper + RECORD_SPEED, # close gripper → raise + RECORD_SPEED, # raise → retract + RECORD_SPEED, # retract → carry pose + RECORD_SPEED, # carry pose → drop + RECORD_SPEED, # drop → HOME + ] + + record_episode_spline(robot, waypoints, speeds, dataset, task) + + # Dwell at HOME for ~0.5 s before next episode + home_action = build_dataset_frame(dataset.features, HOME_POSE, prefix=ACTION) + dt = 1.0 / dataset.fps + for _ in range(int(dataset.fps * 0.5)): + robot.send_action(HOME_POSE) + obs = robot.get_observation() + obs_frame = build_dataset_frame(dataset.features, obs, prefix=OBS_STR) + dataset.add_frame({**obs_frame, **home_action, "task": task}) + precise_sleep(dt) + + +@parser.wrap() +def record_grab(cfg: OmxRecordGrabConfig) -> LeRobotDataset: + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + logger.info(pformat(cfg)) + + robot = make_robot_from_config(cfg.robot) + use_videos = cfg.dataset.video + + teleop_action_processor, _, robot_obs_processor = make_default_processors() + + dataset_features = combine_feature_dicts( + aggregate_pipeline_dataset_features( + pipeline=teleop_action_processor, + initial_features=create_initial_features(action=robot.action_features), + use_videos=use_videos, + ), + aggregate_pipeline_dataset_features( + pipeline=robot_obs_processor, + initial_features=create_initial_features(observation=robot.observation_features), + use_videos=use_videos, + ), + ) + + num_cameras = len(robot.cameras) if hasattr(robot, "cameras") else 0 + dataset = None + + try: + if cfg.resume: + dataset = LeRobotDataset.resume( + cfg.dataset.repo_id, + root=cfg.dataset.root, + streaming_encoding=cfg.dataset.streaming_encoding, + batch_encoding_size=cfg.dataset.video_encoding_batch_size, + vcodec=cfg.dataset.vcodec, + encoder_threads=cfg.dataset.encoder_threads, + image_writer_processes=cfg.dataset.num_image_writer_processes if num_cameras > 0 else 0, + image_writer_threads=cfg.dataset.num_image_writer_threads_per_camera * num_cameras + if num_cameras > 0 + else 0, + ) + else: + cfg.dataset.stamp_repo_id() + dataset = LeRobotDataset.create( + cfg.dataset.repo_id, + cfg.dataset.fps, + root=cfg.dataset.root, + robot_type=robot.name, + features=dataset_features, + use_videos=use_videos, + streaming_encoding=cfg.dataset.streaming_encoding, + batch_encoding_size=cfg.dataset.video_encoding_batch_size, + vcodec=cfg.dataset.vcodec, + encoder_threads=cfg.dataset.encoder_threads, + image_writer_processes=cfg.dataset.num_image_writer_processes if num_cameras > 0 else 0, + image_writer_threads=cfg.dataset.num_image_writer_threads_per_camera * num_cameras + if num_cameras > 0 + else 0, + ) + + robot.connect(calibrate=True) + + rng = np.random.default_rng() + with VideoEncodingManager(dataset): + for episode_idx in range(cfg.dataset.num_episodes): + logger.info(f"=== Episode {episode_idx + 1}/{cfg.dataset.num_episodes} ===") + + logger.info("Step 1: grabbing and placing cube...") + grab_cube(robot) + pan, t = place_cube(robot) + logger.info(f"Cube placed at pan={pan:.1f}, reach={t:.2f}") + + recovery_start = cfg.recovery_prob > 0 and float(rng.random()) < cfg.recovery_prob + logger.info(f"Step 2: recording {'recovery ' if recovery_start else ''}grab episode...") + record_grab_episode( + robot, + dataset, + pan, + t, + cfg.dataset.single_task, + recovery_start=recovery_start, + ) + + dataset.save_episode() + logger.info(f"Episode {episode_idx + 1} saved.") + + finally: + if dataset: + dataset.finalize() + if robot.is_connected: + robot.disconnect() + + if cfg.dataset.push_to_hub and dataset and dataset.num_episodes > 0: + dataset.push_to_hub(tags=cfg.dataset.tags, private=cfg.dataset.private) + + return dataset + + +if __name__ == "__main__": + record_grab() diff --git a/examples/omx/reset_environment.py b/examples/omx/reset_environment.py new file mode 100644 index 000000000..cf8a8c661 --- /dev/null +++ b/examples/omx/reset_environment.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +""" +Auto-reset and cube-grab utility for the OMX robot arm. + +Provides: + - grab_cube(robot): sweep workspace, center cube, close gripper + - place_cube(robot): carry cube to a random position, release + +Standalone usage (run from repo root): + python -m examples.omx.reset_environment --port /dev/ttyACM1 --mode grab + python -m examples.omx.reset_environment --port /dev/ttyACM1 --mode grab_and_place + +Joint range: -100 to 100 for arm joints; gripper: 50 = closed, 80 = open. + +To read current joint values for calibration, add after robot.connect(): + obs = robot.get_observation() + print({k: round(obs[k], 1) for k in JOINT_NAMES}) + robot.disconnect(); raise SystemExit + +Parallel-to-ground IK: wrist_flex = WRIST_HORIZONTAL_OFFSET - shoulder_lift - elbow_flex. +Linear interpolation preserves this constraint between any two poses that satisfy it. +""" + +import argparse +import logging + +import numpy as np + +from lerobot.robots.omx_follower import OmxFollower, OmxFollowerConfig +from lerobot.robots.robot import Robot +from lerobot.utils.robot_utils import precise_sleep + +logger = logging.getLogger(__name__) + +# ── Poses ───────────────────────────────────────────────────────────────────── + +HOME_POSE = { + "shoulder_pan.pos": 0.0, + "shoulder_lift.pos": -50.0, + "elbow_flex.pos": 50.0, + "wrist_flex.pos": 0.0, + "wrist_roll.pos": 0.0, + "gripper.pos": 60.0, +} + +SWEEP_WAYPOINTS = [ + { + "shoulder_pan.pos": -60.0, + "shoulder_lift.pos": 50.0, + "elbow_flex.pos": -60.0, + "wrist_flex.pos": -20.0, + "wrist_roll.pos": 0.0, + "gripper.pos": 60.0, + }, + { + "shoulder_pan.pos": -30.0, + "shoulder_lift.pos": 50.0, + "elbow_flex.pos": -60.0, + "wrist_flex.pos": -5.0, + "wrist_roll.pos": 0.0, + "gripper.pos": 60.0, + }, + { + "shoulder_pan.pos": 20.0, + "shoulder_lift.pos": 50.0, + "elbow_flex.pos": -55.0, + "wrist_flex.pos": -5.0, + "wrist_roll.pos": 0.0, + "gripper.pos": 60.0, + }, +] + +# ── Motion parameters ───────────────────────────────────────────────────────── + +CONTROL_HZ = 30 +APPROACH_SPEED = 50.0 +SWEEP_SPEED = 40.0 + +# ── Grab-sequence parameters ────────────────────────────────────────────────── + +GRAB_PAN = 0.0 +SWEEP_LEFT_PAN = -60.0 +SWEEP_RIGHT_PAN = 60.0 +SWEEP_END_OFFSET = 5.0 # stop before center so the cube isn't pushed past GRAB_PAN +SWEEP_END_PAN_RANGE = (15.0, 20.0) + +SWEEP_LOW_SHOULDER_LIFT = 50.0 +SWEEP_LOW_ELBOW_FLEX_START = -60.0 +SWEEP_LOW_ELBOW_FLEX_END = -55.0 + +SWEEP_HIGH_WRIST_FLEX = -20.0 # wrist tilted up during high approach to clear obstacles + +PUSH_START_SHOULDER_LIFT = 0.0 +PUSH_START_ELBOW_FLEX = 45.0 +PUSH_END_SHOULDER_LIFT = 50.0 +PUSH_END_ELBOW_FLEX = -50.0 +# Subtracted from shoulder_lift during the push sweep to clear the platform surface. +# Does not affect the grab-target interpolation in record_grab.py. +PUSH_RAISE_OFFSET = 5.0 + +WRIST_HORIZONTAL_OFFSET = 0.0 # tune if gripper tilts during push: + tilts nose up, - down +GRIPPER_CLOSE_POS = 50.0 + +PLACE_LEFT_PAN_RANGE = (5.0, 30.0) # random pan range for cube placement on the left side +PLACE_REACH_RANGE = (0.1, 0.7) # 0 = arm retracted (PUSH_START), 1 = fully extended (PUSH_END) + +JOINT_NAMES = [ + "shoulder_pan.pos", + "shoulder_lift.pos", + "elbow_flex.pos", + "wrist_flex.pos", + "wrist_roll.pos", + "gripper.pos", +] + +# ── Helpers ─────────────────────────────────────────────────────────────────── + + +def pose_to_array(pose: dict) -> np.ndarray: + return np.array([pose[k] for k in JOINT_NAMES]) + + +def array_to_pose(arr: np.ndarray) -> dict: + return {k: float(arr[i]) for i, k in enumerate(JOINT_NAMES)} + + +def horizontal_wrist_flex(shoulder_lift: float, elbow_flex: float) -> float: + return WRIST_HORIZONTAL_OFFSET - shoulder_lift - elbow_flex + + +def _low_sweep_pose(pan: float, elbow_flex: float, wrist_flex: float | None = None) -> dict: + sl = SWEEP_LOW_SHOULDER_LIFT + return { + "shoulder_pan.pos": pan, + "shoulder_lift.pos": sl, + "elbow_flex.pos": elbow_flex, + "wrist_flex.pos": horizontal_wrist_flex(sl, elbow_flex) if wrist_flex is None else wrist_flex, + "wrist_roll.pos": 0.0, + "gripper.pos": 60.0, + } + + +def _high_sweep_pose(pan: float) -> dict: + return {**HOME_POSE, "shoulder_pan.pos": pan, "wrist_flex.pos": SWEEP_HIGH_WRIST_FLEX} + + +def _push_pose(shoulder_lift: float, elbow_flex: float, pan: float = GRAB_PAN, gripper: float = 70.0) -> dict: + return { + "shoulder_pan.pos": pan, + "shoulder_lift.pos": shoulder_lift, + "elbow_flex.pos": elbow_flex, + "wrist_flex.pos": horizontal_wrist_flex(shoulder_lift, elbow_flex), + "wrist_roll.pos": 0.0, + "gripper.pos": gripper, + } + + +def move_to_pose(robot: Robot, target: dict, speed: float) -> None: + """Interpolate from current position to target at the given speed (units/s).""" + obs = robot.get_observation() + current = np.array([obs[k] for k in JOINT_NAMES]) + goal = pose_to_array(target) + + max_distance = float(np.max(np.abs(goal - current))) + if max_distance < 0.5: + return + + n_steps = max(1, int(max_distance / speed * CONTROL_HZ)) + dt = 1.0 / CONTROL_HZ + for step in range(1, n_steps + 1): + t = step / n_steps + robot.send_action(array_to_pose(current + t * (goal - current))) + precise_sleep(dt) + + +# ── Sequences ───────────────────────────────────────────────────────────────── + + +def grab_cube(robot: Robot) -> None: + """Left sweep → right sweep → extend arm parallel to ground → close gripper.""" + move_to_pose(robot, HOME_POSE, APPROACH_SPEED) + + for pan, end_pan in [ + (SWEEP_LEFT_PAN, GRAB_PAN - SWEEP_END_OFFSET), + (SWEEP_RIGHT_PAN, GRAB_PAN + SWEEP_END_OFFSET), + ]: + logger.info(f"Sweeping {'left' if pan < 0 else 'right'} → center...") + move_to_pose(robot, _high_sweep_pose(pan), APPROACH_SPEED) + move_to_pose( + robot, _low_sweep_pose(pan, SWEEP_LOW_ELBOW_FLEX_START, wrist_flex=-20.0), APPROACH_SPEED + ) + move_to_pose(robot, _low_sweep_pose(end_pan, SWEEP_LOW_ELBOW_FLEX_END, wrist_flex=0.0), SWEEP_SPEED) + move_to_pose(robot, HOME_POSE, APPROACH_SPEED) + + logger.info("Extending to push cube into gripper...") + move_to_pose( + robot, + _push_pose(PUSH_START_SHOULDER_LIFT - PUSH_RAISE_OFFSET, PUSH_START_ELBOW_FLEX), + APPROACH_SPEED, + ) + move_to_pose( + robot, + _push_pose(PUSH_END_SHOULDER_LIFT - PUSH_RAISE_OFFSET, PUSH_END_ELBOW_FLEX), + SWEEP_SPEED, + ) + + logger.info("Closing gripper...") + move_to_pose( + robot, + _push_pose(PUSH_END_SHOULDER_LIFT, PUSH_END_ELBOW_FLEX, gripper=GRIPPER_CLOSE_POS), + APPROACH_SPEED, + ) + + logger.info("Grab complete.") + + +def place_cube(robot: Robot) -> tuple[float, float]: + """Carry the cube (gripper closed) to a random position on the left side, then release. + + Returns: + (pan, t): pan angle and reach scalar [0, 1] of the placement position. + """ + pan = float(np.random.uniform(*PLACE_LEFT_PAN_RANGE)) + t = float(np.random.uniform(*PLACE_REACH_RANGE)) + sl = PUSH_START_SHOULDER_LIFT + t * (PUSH_END_SHOULDER_LIFT - PUSH_START_SHOULDER_LIFT) + ef = PUSH_START_ELBOW_FLEX + t * (PUSH_END_ELBOW_FLEX - PUSH_START_ELBOW_FLEX) + logger.info(f"Placing cube at pan={pan:.1f}, reach={t:.2f}...") + + move_to_pose(robot, {**HOME_POSE, "gripper.pos": GRIPPER_CLOSE_POS}, APPROACH_SPEED) + move_to_pose( + robot, {**HOME_POSE, "shoulder_pan.pos": pan, "gripper.pos": GRIPPER_CLOSE_POS}, APPROACH_SPEED + ) + move_to_pose(robot, _push_pose(sl, ef, pan=pan, gripper=GRIPPER_CLOSE_POS), APPROACH_SPEED) + move_to_pose(robot, _push_pose(sl, ef, pan=pan, gripper=80.0), APPROACH_SPEED) + move_to_pose(robot, HOME_POSE, APPROACH_SPEED) + logger.info("Place complete.") + return pan, t + + +# ── Entry point ─────────────────────────────────────────────────────────────── + + +def main(): + parser = argparse.ArgumentParser(description="OMX arm reset / grab script") + parser.add_argument("--port", default="/dev/ttyACM1") + parser.add_argument("--robot_id", default="omx_follower") + parser.add_argument("--mode", choices=["grab", "grab_and_place"], default="grab_and_place") + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + + robot = OmxFollower(OmxFollowerConfig(port=args.port, id=args.robot_id)) + robot.connect(calibrate=True) + + try: + if args.mode == "grab": + grab_cube(robot) + elif args.mode == "grab_and_place": + grab_cube(robot) + place_cube(robot) + + finally: + robot.disconnect() + + +if __name__ == "__main__": + main() From 26ff40ddd784280efc133a8e5af1a76e5ac731c2 Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Mon, 11 May 2026 19:47:55 +0200 Subject: [PATCH 08/18] chore(deps): cap torch ceiling at <2.12, pin Linux wheels to cu128 (#3570) * chore(deps): ceiling + cuda * ci: bump cuda version docker image * ci: add cpu wheel to release workflow * chore(deps): update uv.lock * docs: update installation with cuda note --- .github/workflows/release.yml | 3 +- docker/Dockerfile.benchmark.robotwin | 2 +- docker/Dockerfile.internal | 2 +- docs/source/installation.mdx | 50 ++++ pyproject.toml | 20 +- uv.lock | 392 +++++++++++++++++---------- 6 files changed, 327 insertions(+), 142 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aad52cf07..1b4088ad7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -152,13 +152,14 @@ jobs: BASE_VERSION="${VERSION%%-*}" echo "Installing pre-release version $BASE_VERSION from TestPyPI..." uv pip install \ + --torch-backend cpu \ --index-url https://test.pypi.org/simple/ \ --extra-index-url https://pypi.org/simple \ --index-strategy unsafe-best-match \ "lerobot[all]==$BASE_VERSION" else echo "Installing release version $VERSION from PyPI..." - uv pip install "lerobot[all]==$VERSION" + uv pip install --torch-backend cpu "lerobot[all]==$VERSION" fi - name: Check lerobot version run: uv run python -c "import lerobot; print(lerobot.__version__)" diff --git a/docker/Dockerfile.benchmark.robotwin b/docker/Dockerfile.benchmark.robotwin index 8f27c26dd..8c4de6594 100644 --- a/docker/Dockerfile.benchmark.robotwin +++ b/docker/Dockerfile.benchmark.robotwin @@ -35,7 +35,7 @@ USER root ARG ROBOTWIN_SHA=0aeea2d669c0f8516f4d5785f0aa33ba812c14b4 RUN apt-get update \ && apt-get install -y --no-install-recommends \ - cuda-nvcc-12-6 cuda-cudart-dev-12-6 \ + cuda-nvcc-12-8 cuda-cudart-dev-12-8 \ libvulkan1 vulkan-tools \ && mkdir -p /usr/share/vulkan/icd.d \ && echo '{"file_format_version":"1.0.0","ICD":{"library_path":"libGLX_nvidia.so.0","api_version":"1.3.0"}}' \ diff --git a/docker/Dockerfile.internal b/docker/Dockerfile.internal index c48e61bbb..19f0167b3 100644 --- a/docker/Dockerfile.internal +++ b/docker/Dockerfile.internal @@ -18,7 +18,7 @@ # docker build -f docker/Dockerfile.internal -t lerobot-internal . # Configure the base image for CI with GPU access -ARG CUDA_VERSION=12.6.3 +ARG CUDA_VERSION=12.8.1 ARG OS_VERSION=24.04 FROM nvidia/cuda:${CUDA_VERSION}-base-ubuntu${OS_VERSION} diff --git a/docs/source/installation.mdx b/docs/source/installation.mdx index 1d772fc97..ebe32ffdf 100644 --- a/docs/source/installation.mdx +++ b/docs/source/installation.mdx @@ -207,6 +207,56 @@ pip install 'lerobot[feetech]' # Feetech motor support _Multiple extras can be combined (e.g., `.[core_scripts,pi,pusht]`). For a full list of available extras, refer to `pyproject.toml`._ +### PyTorch CUDA variant (Linux only) + +On Linux, the install path determines which CUDA wheel you get. macOS and Windows installs use the PyPI default (MPS / CPU / CUDA-Windows wheel respectively) and can skip this section. + + + + + + +**Source install via `uv` (`uv sync` or `uv pip install -e .`)** + +`torch` and `torchvision` are pinned by the project to the **CUDA 12.8** PyTorch index (`https://download.pytorch.org/whl/cu128`, driver floor **570.86**) — covers Ampere/Ada/Hopper/Blackwell GPUs. No action needed for typical NVIDIA setups. + +To override for a different CUDA variant: + +```bash +uv pip install --force-reinstall torch torchvision \ + --index-url https://download.pytorch.org/whl/cu126 # older drivers; or cu130 for Blackwell on driver ≥ 580 +``` + + + + +**Source install via `pip`/`conda`, or `pip install lerobot` from PyPI** + +PyPI default torch wheel is currently a cu130-bundled Linux wheel, driver floor **580.65**. + +To pick a specific CUDA variant: + +**Using `pip` or `conda`** — install torch first with an explicit index, then lerobot: + +```bash +pip install --index-url https://download.pytorch.org/whl/cu128 torch torchvision +pip install -e ".[all]" # source +# — or — +pip install lerobot # from PyPI +``` + +**Using `uv` to install from PyPI** — one-liner via `--torch-backend` (uv ≥ 0.6): + +```bash +uv pip install --torch-backend cu128 lerobot +``` + +Supported values include `auto`, `cpu`, `cu126`, `cu128`, `cu129`, `cu130`, plus various `rocm*` and `xpu`. Swap as needed for your driver. + + + + + ### Troubleshooting If you encounter build errors, you may need to install additional system dependencies: `cmake`, `build-essential`, and `ffmpeg libs`. diff --git a/pyproject.toml b/pyproject.toml index 7f9ad2141..4deb34034 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,8 +59,8 @@ keywords = ["lerobot", "huggingface", "robotics", "machine learning", "artifici dependencies = [ # Core ML - "torch>=2.7,<2.13.0", - "torchvision>=0.22.0,<0.28.0", + "torch>=2.7,<2.12.0", + "torchvision>=0.22.0,<0.27.0", "numpy>=2.0.0,<2.3.0", # NOTE: Explicitly listing numpy helps the resolver converge faster. Upper bound imposed by opencv-python-headless. "opencv-python-headless>=4.9.0,<4.14.0", "Pillow>=10.0.0,<13.0.0", @@ -99,7 +99,7 @@ dataset = [ "pandas>=2.0.0,<3.0.0", # NOTE: Transitive dependency of datasets "pyarrow>=21.0.0,<30.0.0", # NOTE: Transitive dependency of datasets "lerobot[av-dep]", - "torchcodec>=0.3.0,<0.13.0; sys_platform != 'win32' and (sys_platform != 'linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l')) and (sys_platform != 'darwin' or platform_machine != 'x86_64')", # NOTE: Windows support starts at version 0.7 (needs torch==2.8), ffmpeg>=8 support starts at version 0.8.1 (needs torch==2.9), system-wide ffmpeg support starts at version 0.10 (needs torch==2.10), 0.11 needs torch==2.11, 0.12 needs torch==2.12. + "torchcodec>=0.3.0,<0.12.0; sys_platform != 'win32' and (sys_platform != 'linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l')) and (sys_platform != 'darwin' or platform_machine != 'x86_64')", # NOTE: Windows support starts at version 0.7 (needs torch==2.8), ffmpeg>=8 support starts at version 0.8.1 (needs torch==2.9), system-wide ffmpeg support starts at version 0.10 (needs torch==2.10), 0.11 needs torch==2.11, 0.12 needs torch==2.12. "jsonlines>=4.0.0,<5.0.0", ] training = [ @@ -293,6 +293,20 @@ lerobot-setup-can="lerobot.scripts.lerobot_setup_can:main" lerobot-rollout="lerobot.scripts.lerobot_rollout:main" # ---------------- Tool Configurations ---------------- + +# cu128 wheels keep broad hardware reach; the driver floor is 570.86. +# To use a different CUDA variant, reinstall torch with an explicit index, e.g.: +# uv pip install --force-reinstall torch torchvision \ +# --index-url https://download.pytorch.org/whl/cu130 +[[tool.uv.index]] +name = "pytorch-cu128" +url = "https://download.pytorch.org/whl/cu128" +explicit = true + +[tool.uv.sources] +torch = [{ index = "pytorch-cu128", marker = "sys_platform == 'linux'" }] +torchvision = [{ index = "pytorch-cu128", marker = "sys_platform == 'linux'" }] + [tool.setuptools.package-data] lerobot = ["envs/*.json"] diff --git a/uv.lock b/uv.lock index e2c08bf3e..f09c4a64f 100644 --- a/uv.lock +++ b/uv.lock @@ -60,7 +60,8 @@ dependencies = [ { name = "psutil" }, { name = "pyyaml" }, { name = "safetensors" }, - { name = "torch" }, + { name = "torch", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.11.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ca/14/787e5498cd062640f0f3d92ef4ae4063174f76f9afd29d13fc52a319daae/accelerate-1.13.0.tar.gz", hash = "sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236", size = 402835, upload-time = "2026-03-04T19:34:12.359Z" } wheels = [ @@ -995,20 +996,22 @@ wheels = [ [[package]] name = "cuda-bindings" -version = "13.2.0" +version = "12.9.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cuda-pathfinder", marker = "sys_platform == 'linux'" }, ] wheels = [ - { 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" }, - { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, - { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, - { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, - { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, + { url = "https://files.pythonhosted.org/packages/50/04/8a4d45dc154a8a32982658cc55be291e9778d1197834b15d33427e2f65c1/cuda_bindings-12.9.6-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea331bc47d9988cc61f0ecc5fa8df9dd188b4493ae1c6688bb1ee8ce8ba1af4", size = 7050347, upload-time = "2026-03-11T14:47:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/3b/69/4b0375e1b120dfa7427c31c8420cfdee596ecd03955fd291a96116fa375d/cuda_bindings-12.9.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2b54b95a47104eff56b5155818ab5790e3ccdba8dd51e2928ae56782aaf5b02", size = 7590574, upload-time = "2026-03-11T14:47:37.452Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ad/2d9b80c28deae971ce4bbe991c23b81347a2a8918b2672020d07f070a596/cuda_bindings-12.9.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da30d89db8188b9beb5a6467d72b2f11d1b667ab901d2d373bcde51b97765b21", size = 6950608, upload-time = "2026-03-11T14:47:40.944Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ca/729781d11445cfbacd1af1bf0edfe147c311212cfdf1d5c292e0565fabef/cuda_bindings-12.9.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d1be8bd80b34f51dcbaf138dafd817e888cf2d12c47833019fd933beb32d7ef", size = 7439531, upload-time = "2026-03-11T14:47:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f3/51768221aade33e711dcf7e4a52fdc0d0446c1baf39f6bcc9d69cfbceb0b/cuda_bindings-12.9.6-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48666e666f083a4c4387ffe20594b05e092b535a4453d1e4817d71237d02aa13", size = 6861186, upload-time = "2026-03-11T14:47:46.335Z" }, + { url = "https://files.pythonhosted.org/packages/71/34/14afff4aabe3b5bd84c647dea4a4dfb917c94b8a8df0adb6b1622c2b465b/cuda_bindings-12.9.6-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4f82f8f8061f3a39446bf854c4edd9bcc2d0da3f58d8f6f54541b3e4d5c933d", size = 7356548, upload-time = "2026-03-11T14:47:48.209Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d3/a29faf4fb371c2f43ffda23a938ec0bebf6dbab676350e137ae0f61e5ec0/cuda_bindings-12.9.6-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f00290f9468d2cfeee92aaad2275be32dfd2f4967a97ac0f12314b7e6281ad78", size = 7046617, upload-time = "2026-03-11T14:47:52.46Z" }, + { url = "https://files.pythonhosted.org/packages/2a/97/71e66b2ed65d80f7b70a1538af72d73cd798e22bc93d240d7e69f2366322/cuda_bindings-12.9.6-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3bc6e28cf5d133f72050c515db72876870fb009f1431bcbf45b54a179be2284", size = 7481379, upload-time = "2026-03-11T14:47:54.281Z" }, + { url = "https://files.pythonhosted.org/packages/49/91/c10b575a001aad39c036efd649869aac8d97ef0ba9f1d8ad17b4946b3366/cuda_bindings-12.9.6-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e88d38fdf07cc777dec1afaba8139c2eedb3819063f6b42f1e2ea8516bdd6806", size = 6879714, upload-time = "2026-03-11T14:47:58.095Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9a/998471e76bea78e96d3d7fdf0bc5f46c3210858e81e6d13d8186a9dbb636/cuda_bindings-12.9.6-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df01e34cefd3275170b2ac0426d325271ab435e85f59a69300eacd8ff23d34c", size = 7367020, upload-time = "2026-03-11T14:47:59.781Z" }, ] [[package]] @@ -1021,45 +1024,45 @@ wheels = [ [[package]] name = "cuda-toolkit" -version = "13.0.2" +version = "12.8.1" 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" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/7dce3a0b15b42a3b58e7d96eb22a687d3bf2c44e01d149a6874629cd9938/cuda_toolkit-12.8.1-py2.py3-none-any.whl", hash = "sha256:adc7906af4ecbf9a352f9dca5734eceb21daec281ccfcf5675e1d2f724fc2cba", size = 2283, upload-time = "2025-08-13T02:03:07.842Z" }, ] [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "sys_platform == 'linux'" }, ] cufft = [ - { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "sys_platform == 'linux'" }, ] cufile = [ - { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "sys_platform == 'linux'" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "sys_platform == 'linux'" }, ] curand = [ - { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "sys_platform == 'linux'" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "sys_platform == 'linux'" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "sys_platform == 'linux'" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -1459,7 +1462,8 @@ version = "2.8.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "einops" }, - { name = "torch" }, + { name = "torch", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.11.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/b2/8d76c41ad7974ee264754709c22963447f7f8134613fd9ce80984ed0dab7/flash_attn-2.8.3.tar.gz", hash = "sha256:1e71dd64a9e0280e0447b8a0c2541bad4bf6ac65bdeaa2f90e51a9e57de0370d", size = 8447812, upload-time = "2025-08-15T08:28:12.911Z" } @@ -2664,8 +2668,10 @@ dependencies = [ { name = "safetensors" }, { name = "setuptools" }, { name = "termcolor" }, - { name = "torch" }, - { name = "torchvision" }, + { name = "torch", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.11.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "torchvision", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torchvision", version = "0.26.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, { name = "tqdm" }, ] @@ -3194,10 +3200,12 @@ requires-dist = [ { name = "teleop", marker = "extra == 'phone'", specifier = ">=0.1.0,<0.2.0" }, { name = "termcolor", specifier = ">=2.4.0,<4.0.0" }, { name = "timm", marker = "extra == 'groot'", specifier = ">=1.0.0,<1.1.0" }, - { name = "torch", specifier = ">=2.7,<2.13.0" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux' and extra == 'dataset') or (platform_machine != 'x86_64' and sys_platform == 'darwin' and extra == 'dataset') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'dataset')", specifier = ">=0.3.0,<0.13.0" }, + { name = "torch", marker = "sys_platform != 'linux'", specifier = ">=2.7,<2.12.0" }, + { name = "torch", marker = "sys_platform == 'linux'", specifier = ">=2.7,<2.12.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux' and extra == 'dataset') or (platform_machine != 'x86_64' and sys_platform == 'darwin' and extra == 'dataset') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'dataset')", specifier = ">=0.3.0,<0.12.0" }, { name = "torchdiffeq", marker = "extra == 'wallx'", specifier = ">=0.2.4,<0.3.0" }, - { name = "torchvision", specifier = ">=0.22.0,<0.28.0" }, + { name = "torchvision", marker = "sys_platform != 'linux'", specifier = ">=0.22.0,<0.27.0" }, + { name = "torchvision", marker = "sys_platform == 'linux'", specifier = ">=0.22.0,<0.27.0", index = "https://download.pytorch.org/whl/cu128" }, { name = "tqdm", specifier = ">=4.66.0,<5.0.0" }, { name = "transformers", marker = "extra == 'transformers-dep'", specifier = ">=5.4.0,<5.6.0" }, { name = "wandb", marker = "extra == 'training'", specifier = ">=0.24.0,<0.25.0" }, @@ -4043,152 +4051,152 @@ wheels = [ ] [[package]] -name = "nvidia-cublas" -version = "13.1.0.3" +name = "nvidia-cublas-cu12" +version = "12.8.4.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/29/99/db44d685f0e257ff0e213ade1964fc459b4a690a73293220e98feb3307cf/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0", size = 590537124, upload-time = "2025-03-07T01:43:53.556Z" }, + { 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" -version = "13.0.85" +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" source = { registry = "https://pypi.org/simple" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/d5/1f/b3bd73445e5cb342727fd24fe1f7b748f690b460acadc27ea22f904502c8/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed", size = 9533318, upload-time = "2025-03-07T01:40:10.421Z" }, + { 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" -version = "13.0.88" +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" source = { registry = "https://pypi.org/simple" } wheels = [ - { 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" }, + { 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/eb/d1/e50d0acaab360482034b84b6e27ee83c6738f7d32182b987f9c7a4e32962/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8", size = 43106076, upload-time = "2025-03-07T01:41:59.817Z" }, ] [[package]] -name = "nvidia-cuda-runtime" -version = "13.0.96" +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" source = { registry = "https://pypi.org/simple" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/7c/75/f865a3b236e4647605ea34cc450900854ba123834a5f1598e160b9530c3a/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d", size = 965265, upload-time = "2025-03-07T01:39:43.533Z" }, + { 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-cu13" +name = "nvidia-cudnn-cu12" version = "9.19.0.56" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/09/b8/277c51962ee46fa3e5b203ac5f76107c650f781d6891e681e28e6f3e9fe6/nvidia_cudnn_cu12-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:08caaf27fe556aca82a3ee3b5aa49a77e7de0cfcb7ff4e5c29da426387a8267e", size = 656910700, upload-time = "2026-02-03T20:40:25.508Z" }, + { url = "https://files.pythonhosted.org/packages/c5/41/65225d42fba06fb3dd3972485ea258e7dd07a40d6e01c95da6766ad87354/nvidia_cudnn_cu12-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ac6ad90a075bb33a94f2b4cf4622eac13dd4dc65cf6dd9c7572a318516a36625", size = 657906812, upload-time = "2026-02-03T20:44:12.638Z" }, ] [[package]] -name = "nvidia-cufft" -version = "12.0.0.61" +name = "nvidia-cufft-cu12" +version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/60/bc/7771846d3a0272026c416fbb7e5f4c1f146d6d80704534d0b187dd6f4800/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a", size = 193109211, upload-time = "2025-03-07T01:44:56.873Z" }, + { 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" -version = "1.15.1.6" +name = "nvidia-cufile-cu12" +version = "1.13.1.3" source = { registry = "https://pypi.org/simple" } wheels = [ - { 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" }, + { 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/1e/f5/5607710447a6fe9fd9b3283956fceeee8a06cda1d2f56ce31371f595db2a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a", size = 1120705, upload-time = "2025-03-07T01:45:41.434Z" }, ] [[package]] -name = "nvidia-curand" -version = "10.4.0.35" +name = "nvidia-curand-cu12" +version = "10.3.9.90" source = { registry = "https://pypi.org/simple" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/45/5e/92aa15eca622a388b80fbf8375d4760738df6285b1e92c43d37390a33a9a/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd", size = 63625754, upload-time = "2025-03-07T01:46:10.735Z" }, + { 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" -version = "12.0.4.66" +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, - { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/c8/32/f7cd6ce8a7690544d084ea21c26e910a97e077c9b7f07bf5de623ee19981/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0", size = 267229841, upload-time = "2025-03-07T01:46:54.356Z" }, + { 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" -version = "12.6.3.3" +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/bc/f7/cd777c4109681367721b00a106f491e0d0d15cfa1fd59672ce580ce42a97/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc", size = 288117129, upload-time = "2025-03-07T01:47:40.407Z" }, + { 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-cu13" -version = "0.8.0" +name = "nvidia-cusparselt-cu12" +version = "0.7.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/73/b9/598f6ff36faaece4b3c50d26f50e38661499ff34346f00e057760b35cc9d/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5", size = 283835557, upload-time = "2025-02-26T00:16:54.265Z" }, + { 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-cu13" +name = "nvidia-nccl-cu12" version = "2.28.9" source = { registry = "https://pypi.org/simple" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/08/c4/120d2dfd92dff2c776d68f361ff8705fdea2ca64e20b612fab0fd3f581ac/nvidia_nccl_cu12-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:50a36e01c4a090b9f9c47d92cec54964de6b9fcb3362d0e19b8ffc6323c21b60", size = 296766525, upload-time = "2025-11-18T05:49:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/44dbb46b3d1b0ec61afda8e84837870f2f9ace33c564317d59b70bc19d3e/nvidia_nccl_cu12-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:485776daa8447da5da39681af455aa3b2c2586ddcf4af8772495e7c532c7e5ab", size = 296782137, upload-time = "2025-11-18T05:49:34.248Z" }, ] [[package]] -name = "nvidia-nvjitlink" -version = "13.0.88" +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" source = { registry = "https://pypi.org/simple" } wheels = [ - { 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" }, + { 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/2a/a2/8cee5da30d13430e87bf99bb33455d2724d0a4a9cb5d7926d80ccb96d008/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7", size = 38386204, upload-time = "2025-03-07T01:49:43.612Z" }, ] [[package]] -name = "nvidia-nvshmem-cu13" +name = "nvidia-nvshmem-cu12" version = "3.4.5" source = { registry = "https://pypi.org/simple" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/1d/6a/03aa43cc9bd3ad91553a88b5f6fb25ed6a3752ae86ce2180221962bc2aa5/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b48363fc6964dede448029434c6abed6c5e37f823cb43c3bcde7ecfc0457e15", size = 138936938, upload-time = "2025-09-06T00:32:05.589Z" }, + { 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" -version = "13.0.85" +name = "nvidia-nvtx-cu12" +version = "12.8.90" source = { registry = "https://pypi.org/simple" } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/10/c0/1b303feea90d296f6176f32a2a70b5ef230f9bdeb3a72bddb0dc922dc137/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615", size = 91161, upload-time = "2025-03-07T01:42:23.922Z" }, + { 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]] @@ -4405,7 +4413,8 @@ dependencies = [ { name = "psutil" }, { name = "pyyaml" }, { name = "safetensors" }, - { name = "torch" }, + { name = "torch", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.11.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, { name = "tqdm" }, { name = "transformers" }, ] @@ -5632,8 +5641,8 @@ dependencies = [ { name = "tensorboard", marker = "sys_platform == 'linux'" }, { name = "tensorboardx", marker = "sys_platform == 'linux'" }, { name = "termcolor", marker = "sys_platform == 'linux'" }, - { name = "torch", marker = "sys_platform == 'linux'" }, - { name = "torchvision", marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.11.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "torchvision", version = "0.26.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, { name = "tqdm", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/c3/44b1d1ea4bcb4bbed43d19e09505f4142714451ded74020d4f679cdc89fb/robomimic-0.2.0.tar.gz", hash = "sha256:ee3bb5cf9c3e1feead6b57b43c5db738fd0a8e0c015fdf6419808af8fffdc463", size = 192919, upload-time = "2021-12-17T19:00:33.279Z" } @@ -6119,7 +6128,7 @@ name = "thop" version = "0.1.1.post2209072238" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "torch", marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.11.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/bb/0f/72beeab4ff5221dc47127c80f8834b4bcd0cb36f6ba91c0b1d04a1233403/thop-0.1.1.post2209072238-py3-none-any.whl", hash = "sha256:01473c225231927d2ad718351f78ebf7cffe6af3bed464c4f1ba1ef0f7cdda27", size = 15443, upload-time = "2022-09-07T14:38:37.211Z" }, @@ -6145,8 +6154,10 @@ dependencies = [ { name = "huggingface-hub" }, { name = "pyyaml" }, { name = "safetensors" }, - { name = "torch" }, - { name = "torchvision" }, + { name = "torch", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.11.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "torchvision", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torchvision", version = "0.26.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/54/ece85b0eef3700c90db8271a43669b05a0ebbe2edb1962329c34374a297e/timm-1.0.27.tar.gz", hash = "sha256:315dfe63186ca9fb7ff941268941231fd5be259f2b4bb4afa28560ae1015cb9a", size = 2439861, upload-time = "2026-05-08T19:38:36.844Z" } wheels = [ @@ -6204,45 +6215,101 @@ wheels = [ name = "torch" version = "2.11.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.15' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "(python_full_version == '3.14.*' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version == '3.13.*' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", + "(python_full_version < '3.13' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "(python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'win32'", + "(python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'win32')", + "(python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "(python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", +] dependencies = [ - { 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" }, - { 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" }, + { name = "filelock", marker = "sys_platform != 'linux'" }, + { name = "fsspec", marker = "sys_platform != 'linux'" }, + { name = "jinja2", marker = "sys_platform != 'linux'" }, + { name = "networkx", marker = "sys_platform != 'linux'" }, + { name = "setuptools", marker = "sys_platform != 'linux'" }, + { name = "sympy", marker = "sys_platform != 'linux'" }, + { name = "typing-extensions", marker = "sys_platform != 'linux'" }, ] wheels = [ { 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" }, { url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" }, - { url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" }, { url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" }, { url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" }, - { url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" }, { url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" }, { url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" }, - { url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" }, { url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" }, { url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" }, - { url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" }, { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, ] +[[package]] +name = "torch" +version = "2.11.0+cu128" +source = { registry = "https://download.pytorch.org/whl/cu128" } +resolution-markers = [ + "python_full_version >= '3.15' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'linux'", + "(python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'armv7l' and sys_platform == 'linux')", + "(python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", + "(python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", + "(python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'armv7l' and sys_platform == 'linux')", +] +dependencies = [ + { 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", marker = "sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "networkx", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "sys_platform == 'linux'" }, + { name = "setuptools", marker = "sys_platform == 'linux'" }, + { name = "sympy", marker = "sys_platform == 'linux'" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9c8f38efee365cb9d334de8a83ce52fc7e5fc9e5a7b0853285efa1b69e00b0f2", upload-time = "2026-04-27T17:41:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d252cf975fb18c94a85336323ad425f473df56dab35a44b00399bd70c7a3b997", upload-time = "2026-04-27T17:42:06Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7db3580106bba044da5b8950f3fb8fe5f31999eaab3f6a3aa2ac5d202c3684d2", upload-time = "2026-04-27T17:45:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:db964b33c55035a72ab3e2162287af8f1cc276039c65d015740cc88c26dcedf7", upload-time = "2026-04-27T17:46:18Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd1cf1005c5fe419194ee294b7b584ba5ad0f2fb1778b3fe5a7b9c3f4617ddbc", upload-time = "2026-04-27T17:50:01Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:74b628dbc71603977b09f4e140792c6e997081a35ef3421555f3f6e201b81210", upload-time = "2026-04-27T17:50:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:baa52f7b8a53cab16587b10f1c27d1000ca033f97236878b685b75d5a1b92408", upload-time = "2026-04-27T17:54:24Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d389a850677f0d24dafae1573644034428d8d3b9c80b51d55ba62fed7e6c8777", upload-time = "2026-04-27T17:55:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:06849e9311dbb0617c97557d9c26c99a9e1c4f2ac9cb8e9b6d9b420d522acb91", upload-time = "2026-04-27T17:58:48Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:169a9987e1f84f0c5eee07544b3a34827a163ac9180e23abf0c3548f1335762c", upload-time = "2026-04-27T17:59:26Z" }, +] + [[package]] name = "torchcodec" version = "0.11.1" @@ -6265,7 +6332,8 @@ version = "0.2.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "scipy" }, - { name = "torch" }, + { name = "torch", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.11.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/ec/a40aa124660f0ee65e6760cb53df6a82ad91a1a3ef1da5e747f1336644dd/torchdiffeq-0.2.5.tar.gz", hash = "sha256:b50d3760d13fd138dcceac651f4b80396f44fefcebd037a033fecfeaa9cc12e7", size = 31197, upload-time = "2024-11-21T20:20:11.552Z" } wheels = [ @@ -6276,34 +6344,86 @@ wheels = [ name = "torchvision" version = "0.26.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.15' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "(python_full_version == '3.14.*' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version == '3.13.*' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", + "(python_full_version < '3.13' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", + "(python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'win32'", + "(python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'win32')", + "(python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'win32'", + "(python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", +] dependencies = [ - { name = "numpy" }, - { name = "pillow" }, - { name = "torch" }, + { name = "numpy", marker = "sys_platform != 'linux'" }, + { name = "pillow", marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, ] wheels = [ { 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" }, { url = "https://files.pythonhosted.org/packages/da/80/0762f77f53605d10c9477be39bb47722cc8e383bbbc2531471ce0e396c07/torchvision-0.26.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5d63dd43162691258b1b3529b9041bac7d54caa37eae0925f997108268cbf7c4", size = 1860809, upload-time = "2026-03-23T18:12:47.629Z" }, - { url = "https://files.pythonhosted.org/packages/e6/81/0b3e58d1478c660a5af4268713486b2df7203f35abd9195fea87348a5178/torchvision-0.26.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a39c7a26538c41fda453f9a9692b5ff9b35a5437db1d94f3027f6f509c160eac", size = 7727494, upload-time = "2026-03-23T18:12:46.062Z" }, - { url = "https://files.pythonhosted.org/packages/b6/dc/d9ab5d29115aa05e12e30f1397a3eeae1d88a511241dc3bce48dc4342675/torchvision-0.26.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b7e6213620bbf97742e5f79832f9e9d769e6cf0f744c5b53dad80b76db633691", size = 7521747, upload-time = "2026-03-23T18:12:36.815Z" }, { url = "https://files.pythonhosted.org/packages/a9/1b/f1bc86a918c5f6feab1eeff11982e2060f4704332e96185463d27855bdf5/torchvision-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:4280c35ec8cba1fcc8294fb87e136924708726864c379e4c54494797d86bc474", size = 4319880, upload-time = "2026-03-23T18:12:38.168Z" }, { url = "https://files.pythonhosted.org/packages/66/28/b4ad0a723ed95b003454caffcc41894b34bd8379df340848cae2c33871de/torchvision-0.26.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:358fc4726d0c08615b6d83b3149854f11efb2a564ed1acb6fce882e151412d23", size = 1951973, upload-time = "2026-03-23T18:12:48.781Z" }, - { url = "https://files.pythonhosted.org/packages/71/e2/7a89096e6cf2f3336353b5338ba925e0addf9d8601920340e6bdf47e8eb3/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:3daf9cc149cf3cdcbd4df9c59dae69ffca86c6823250442c3bbfd63fc2e26c61", size = 7728679, upload-time = "2026-03-23T18:12:26.196Z" }, - { url = "https://files.pythonhosted.org/packages/69/1d/4e1eebc17d18ce080a11dcf3df3f8f717f0efdfa00983f06e8ba79259f61/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:82c3965eca27e86a316e31e4c3e5a16d353e0bcbe0ef8efa2e66502c54493c4b", size = 7609138, upload-time = "2026-03-23T18:12:35.327Z" }, { url = "https://files.pythonhosted.org/packages/f3/a4/f1155e943ae5b32400d7000adc81c79bb0392b16ceb33bcf13e02e48cced/torchvision-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ebc043cc5a4f0bf22e7680806dbba37ffb19e70f6953bbb44ed1a90aeb5c9bea", size = 4248202, upload-time = "2026-03-23T18:12:41.423Z" }, { url = "https://files.pythonhosted.org/packages/7f/c8/9bffa9c7f7bdf95b2a0a2dc535c290b9f1cc580c3fb3033ab1246ffffdeb/torchvision-0.26.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:eb61804eb9dbe88c5a2a6c4da8dec1d80d2d0a6f18c999c524e32266cb1ebcd3", size = 1860813, upload-time = "2026-03-23T18:12:39.636Z" }, - { url = "https://files.pythonhosted.org/packages/7b/ac/48f28ffd227991f2e14f4392dde7e8dc14352bb9428c1ef4a4bbf5f7ed85/torchvision-0.26.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:9a904f2131cbfadab4df828088a9f66291ad33f49ff853872aed1f86848ef776", size = 7727777, upload-time = "2026-03-23T18:12:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/a4/21/a2266f7f1b0e58e624ff15fd6f01041f59182c49551ece0db9a183071329/torchvision-0.26.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0f3e572efe62ad645017ea847e0b5e4f2f638d4e39f05bc011d1eb9ac68d4806", size = 7522174, upload-time = "2026-03-23T18:12:29.565Z" }, { url = "https://files.pythonhosted.org/packages/fc/ba/1666f90bc0bdd77aaa11dcc42bb9f621a9c3668819c32430452e3d404730/torchvision-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:114bec0c0e98aa4ba446f63e2fe7a2cbca37b39ac933987ee4804f65de121800", size = 4348469, upload-time = "2026-03-23T18:12:24.44Z" }, { url = "https://files.pythonhosted.org/packages/45/8f/1f0402ac55c2ae15651ff831957d083fe70b2d12282e72612a30ba601512/torchvision-0.26.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:b7d3e295624a28b3b1769228ce1345d94cf4d390dd31136766f76f2d20f718da", size = 1860826, upload-time = "2026-03-23T18:12:34.1Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6a/18a582fe3c5ee26f49b5c9fb21ad8016b4d1c06d10178894a58653946fda/torchvision-0.26.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7058c5878262937e876f20c25867b33724586aa4499e2853b2d52b99a5e51953", size = 7729089, upload-time = "2026-03-23T18:12:31.394Z" }, - { url = "https://files.pythonhosted.org/packages/c5/9b/f7e119b59499edc00c55c03adc9ec3bd96144d9b81c46852c431f9c64a9a/torchvision-0.26.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8008474855623c6ba52876589dc52df0aa66e518c25eca841445348e5f79844c", size = 7522704, upload-time = "2026-03-23T18:12:20.301Z" }, { url = "https://files.pythonhosted.org/packages/d0/6a/09f3844c10643f6c0de5d95abc863420cfaf194c88c7dffd0ac523e2015f/torchvision-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e9d0e022c19a78552fb055d0414d47fecb4a649309b9968573daea160ba6869c", size = 4454275, upload-time = "2026-03-23T18:12:27.487Z" }, ] +[[package]] +name = "torchvision" +version = "0.26.0+cu128" +source = { registry = "https://download.pytorch.org/whl/cu128" } +resolution-markers = [ + "python_full_version >= '3.15' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'linux'", + "(python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'armv7l' and sys_platform == 'linux')", + "(python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", + "(python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", + "(python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'armv7l' and sys_platform == 'linux')", +] +dependencies = [ + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "pillow", marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.11.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63e35234aed13b6edda37056f417b5c281249669db631e706811917af36b21d7", upload-time = "2026-04-09T23:21:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ccf26b4b659cfce6f2208cb8326071d51c70219a34856dfdf468d1e19af52c0d", upload-time = "2026-03-23T15:36:22Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c4a9cacd521f2a4df0bcd9d8e96704771b928f478f1f3067e4085bb53a1da298", upload-time = "2026-04-09T23:21:37Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cb1f6184a7ba30fba40580e1a01a6604a86c55e79fdda187f40116ee680441ec", upload-time = "2026-03-23T15:36:22Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e594732552a8c2fee2ace9c6475c6c6904fc44ccca622ee6765a89a045416a44", upload-time = "2026-04-09T23:21:38Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6168abc019803ac9e97efce27eafd2fdb33db04dcc54a86039537729e5047b29", upload-time = "2026-03-23T15:36:23Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b3865fa227661dd75b7b28c96d3d14e739bd08bf0614132758922fe0e7206f91", upload-time = "2026-04-09T23:21:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:aac647c9130f1f25f5c8f5bca3d95cfd96bdfac93ab54529690b088e64e4fa64", upload-time = "2026-03-23T15:36:23Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e2ee9e16ee4518292694537fcbd20d2d27044e381d92b864f637e82795796a84", upload-time = "2026-04-09T23:21:40Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b5772c55bfda4377df8f1930d43c4e0231ef231b0228eade4b227c8d3ba6e34e", upload-time = "2026-03-23T15:36:23Z" }, +] + [[package]] name = "tornado" version = "6.5.5" From e963e5a0c496f4760f0c13f166de159cc33cebd1 Mon Sep 17 00:00:00 2001 From: Khalil Meftah Date: Tue, 12 May 2026 15:49:54 +0200 Subject: [PATCH 09/18] RL stack refactoring (#3075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: RL stack refactoring — RLAlgorithm, RLTrainer, DataMixer, and SAC restructuring * chore: clarify torch.compile disabled note in SACAlgorithm * fix(teleop): keyboard EE teleop not registering special keys and losing intervention state Fixes #2345 Co-authored-by: jpizarrom * fix: remove leftover normalization calls from reward classifier predict_reward Fixes #2355 * fix: add thread synchronization to ReplayBuffer to prevent race condition between add() and sample() * refactor: update SACAlgorithm to pass action_dim to _init_critics and fix encoder reference * perf: remove redundant CPU→GPU→CPU transition move in learner * Fix: add kwargs in reward classifier __init__() * fix: include IS_INTERVENTION in complementary_info sent to learner for offline replay buffer * fix: add try/finally to control_loop to ensure image writer cleanup on exit * fix: use string key for IS_INTERVENTION in complementary_info to avoid torch.load serialization error * fix: skip tests that require grpc if not available * fix(tests): ensure tensor stats comparison accounts for reshaping in normalization tests * fix(tests): skip tests that require grpc if not available * refactor(rl): expose public API in rl/__init__ and use relative imports in sub-packages * fix(config): update vision encoder model name to lerobot/resnet10 * fix(sac): clarify torch.compile status * refactor(rl): update shutdown_event type hints from 'any' to 'Any' for consistency and clarity * refactor(sac): simplify optimizer return structure * perf(rl): use async iterators in OnlineOfflineMixer.get_iterator * refactor(sac): decouple algorithm hyperparameters from policy config * update losses names in tests * fix docstring * remove unused type alias * fix test for flat dict structure * refactor(policies): rename policies/sac → policies/gaussian_actor * refactor(rl/sac): consolidate hyperparameter ownership and clean up discrete critic * perf(observation_processor): add CUDA support for image processing * fix(rl): correctly wire HIL-SERL gripper penalty through processor pipeline (cherry picked from commit 9c2af818ff4bfef2603348e0609aa249c3ff62b1) * fix(rl): add time limit processor to environment pipeline (cherry picked from commit cd105f65cb213c4a9c9768926cc3304ca52eb5f4) * fix(rl): clarify discrete gripper action mapping in GripperVelocityToJoint for SO100 (cherry picked from commit 494f469a2b9dfb792dde6d9d79d8646ef4fcff54) * fix(rl): update neutral gripper action (cherry picked from commit 9c9064e5befe82e981286c6562194f524e16045e) * fix(rl): merge environment and action-processor info in transition processing (cherry picked from commit 30e1886b6466b8753ec41b3016c09a17dd3e960b) * fix(rl): mirror gym_manipulator in actor (cherry picked from commit d2a046dfc5b6f79df34577aa45f32403d897c0a3) * fix(rl): postprocess action in actor (cherry picked from commit c2556439e550ee3fe5bae6060c57cf227101fcaf) * fix(rl): improve action processing for discrete and continuous actions (cherry picked from commit f887ab3f6ace140c4ea6b6186c26473d785b0727) * fix(rl): enhance intervention handling in actor and learner (cherry picked from commit ef8bfffbd72e9d0951de576553f89c7c281315de) * Revert "perf(observation_processor): add CUDA support for image processing" This reverts commit 38b88c414cdc1f53ebaab3211e688fe87522b732. * refactor(rl): make algorithm a nested config so all SAC hyperparameters are JSON-addressable * refactor(rl): add make_algorithm_config function for RLAlgorithmConfig instantiation * refactor(rl): add type property to RLAlgorithmConfig for better clarity * refactor(rl): make RLAlgorithmConfig an abstract base class for better extensibility * refactor(tests): remove grpc import checks from test files for cleaner code * fix(tests): gate RL tests on the `datasets` extra * refactor: simplify docstrings for clarity and conciseness across multiple files * fix(rl): update gripper position key and handle action absence during reset * fix(rl): record pre-step observation so (obs, action, next.reward) align in gym_manipulator dataset * refactor: clean up import statements * chore: address reviewer comments * chore: improve visual stats reshaping logic and update docstring for clarity * refactor: enforce mandatory config_class and name attributes in RLAlgorithm * refactor: implement NotImplementedError for abstract methods in RLAlgorithm and DataMixer * refactor: replace build_algorithm with make_algorithm for SACAlgorithmConfig and update related tests * refactor: add require_package calls for grpcio and gym-hil in relevant modules * refactor(rl): move grpcio guards to runtime entry points * feat(rl): consolidate HIL-SERL checkpoint into HF-style components Make `RLAlgorithmConfig` and `RLAlgorithm` `HubMixin`s, add abstract `state_dict()` / `load_state_dict()` for critic ensemble, target nets and `log_alpha`, and persist them as a sibling `algorithm/` component next to `pretrained_model/`. Replace the pickled `training_state.pt` with an enriched `training_step.json` carrying `step` and `interaction_step`, so resume restores actor + critics + target nets + temperature + optimizers + RNG + counters from HF-standard files. * refactor(rl): move actor weight-sync wire format from policy to algorithm * refactor(rl): update type hints for learner and actor functions * refactor(rl): hoist grpcio guard to module top in actor/learner * chore(rl): manage import pattern in actor (#3564) * chore(rl): manage import pattern in actor * chore(rl): optional grpc imports in learner; quote grpc ServicerContext types --------- Co-authored-by: Khalil Meftah * update uv.lock * chore(doc): update doc --------- Co-authored-by: jpizarrom Co-authored-by: Steven Palma --- docs/source/hilserl.mdx | 77 +- examples/tutorial/rl/hilserl_example.py | 45 +- pyproject.toml | 2 +- src/lerobot/common/train_utils.py | 1 + src/lerobot/configs/train.py | 7 - src/lerobot/policies/__init__.py | 10 +- src/lerobot/policies/factory.py | 22 +- .../{sac => gaussian_actor}/__init__.py | 8 +- .../configuration_gaussian_actor.py} | 96 +-- .../modeling_gaussian_actor.py} | 495 ++----------- .../processor_gaussian_actor.py} | 10 +- src/lerobot/processor/hil_processor.py | 42 +- src/lerobot/processor/normalize_processor.py | 21 + .../classifier/configuration_classifier.py | 2 +- .../rewards/classifier/modeling_classifier.py | 1 + src/lerobot/rl/__init__.py | 42 +- src/lerobot/rl/actor.py | 191 +++-- src/lerobot/rl/algorithms/__init__.py | 20 + src/lerobot/rl/algorithms/base.py | 207 ++++++ src/lerobot/rl/algorithms/configs.py | 138 ++++ src/lerobot/rl/algorithms/factory.py | 99 +++ src/lerobot/rl/algorithms/sac/__init__.py | 18 + .../rl/algorithms/sac/configuration_sac.py | 99 +++ .../rl/algorithms/sac/sac_algorithm.py | 672 ++++++++++++++++++ src/lerobot/rl/buffer.py | 6 +- src/lerobot/rl/crop_dataset_roi.py | 4 +- src/lerobot/rl/data_sources/__init__.py | 19 + src/lerobot/rl/data_sources/data_mixer.py | 97 +++ src/lerobot/rl/eval_policy.py | 2 +- src/lerobot/rl/gym_manipulator.py | 181 +++-- src/lerobot/rl/learner.py | 432 ++++------- src/lerobot/rl/learner_service.py | 31 +- src/lerobot/rl/train_rl.py | 50 ++ src/lerobot/rl/trainer.py | 101 +++ .../so_follower/robot_kinematic_processor.py | 11 +- .../teleoperators/keyboard/teleop_keyboard.py | 11 +- .../templates/lerobot_modelcard_template.md | 4 +- src/lerobot/types.py | 1 + src/lerobot/utils/constants.py | 1 + src/lerobot/utils/import_utils.py | 1 + ...onfig.py => test_gaussian_actor_config.py} | 55 +- tests/policies/test_gaussian_actor_policy.py | 528 ++++++++++++++ tests/policies/test_sac_policy.py | 546 -------------- ...or.py => test_gaussian_actor_processor.py} | 48 +- tests/processor/test_normalize_processor.py | 21 +- tests/rewards/test_modeling_classifier.py | 13 - tests/rl/test_actor_learner.py | 171 ++++- tests/rl/test_data_mixer.py | 89 +++ tests/rl/test_queue.py | 2 +- tests/rl/test_sac_algorithm.py | 606 ++++++++++++++++ tests/rl/test_trainer.py | 133 ++++ tests/utils/test_process.py | 2 +- tests/utils/test_replay_buffer.py | 1 - uv.lock | 7 + 54 files changed, 3755 insertions(+), 1744 deletions(-) rename src/lerobot/policies/{sac => gaussian_actor}/__init__.py (67%) rename src/lerobot/policies/{sac/configuration_sac.py => gaussian_actor/configuration_gaussian_actor.py} (73%) rename src/lerobot/policies/{sac/modeling_sac.py => gaussian_actor/modeling_gaussian_actor.py} (54%) rename src/lerobot/policies/{sac/processor_sac.py => gaussian_actor/processor_gaussian_actor.py} (92%) create mode 100644 src/lerobot/rl/algorithms/__init__.py create mode 100644 src/lerobot/rl/algorithms/base.py create mode 100644 src/lerobot/rl/algorithms/configs.py create mode 100644 src/lerobot/rl/algorithms/factory.py create mode 100644 src/lerobot/rl/algorithms/sac/__init__.py create mode 100644 src/lerobot/rl/algorithms/sac/configuration_sac.py create mode 100644 src/lerobot/rl/algorithms/sac/sac_algorithm.py create mode 100644 src/lerobot/rl/data_sources/__init__.py create mode 100644 src/lerobot/rl/data_sources/data_mixer.py create mode 100644 src/lerobot/rl/train_rl.py create mode 100644 src/lerobot/rl/trainer.py rename tests/policies/{test_sac_config.py => test_gaussian_actor_config.py} (81%) create mode 100644 tests/policies/test_gaussian_actor_policy.py delete mode 100644 tests/policies/test_sac_policy.py rename tests/processor/{test_sac_processor.py => test_gaussian_actor_processor.py} (89%) create mode 100644 tests/rl/test_data_mixer.py create mode 100644 tests/rl/test_sac_algorithm.py create mode 100644 tests/rl/test_trainer.py diff --git a/docs/source/hilserl.mdx b/docs/source/hilserl.mdx index 5b9439d51..76e985cfe 100644 --- a/docs/source/hilserl.mdx +++ b/docs/source/hilserl.mdx @@ -62,7 +62,7 @@ pip install -e ".[hilserl]" ### Understanding Configuration -The training process begins with proper configuration for the HILSerl environment. The main configuration class is `GymManipulatorConfig` in `lerobot/rl/gym_manipulator.py`, which contains nested `HILSerlRobotEnvConfig` and `DatasetConfig`. The configuration is organized into focused, nested sub-configs: +The training process begins with proper configuration for the HILSERl environment. The main configuration class is `GymManipulatorConfig` in `lerobot/rl/gym_manipulator.py`, which contains nested `HILSerlRobotEnvConfig` (defined in `lerobot/envs/configs.py`) and `DatasetConfig`. The configuration is organized into focused, nested sub-configs: ```python @@ -95,6 +95,7 @@ class HILSerlProcessorConfig: class ObservationConfig: add_joint_velocity_to_observation: bool = False # Add joint velocities to state add_current_to_observation: bool = False # Add motor currents to state + add_ee_pose_to_observation: bool = False # Add end-effector pose to state display_cameras: bool = False # Display camera feeds during execution class ImagePreprocessingConfig: @@ -326,14 +327,22 @@ lerobot-find-joint-limits \ Max joint positions [-20.0, -20.0, -20.0, -20.0, -20.0, -20.0] Min joint positions [50.0, 50.0, 50.0, 50.0, 50.0, 50.0] ``` -3. Use these values in the configuration of your teleoperation device (TeleoperatorConfig) under the `end_effector_bounds` field +3. Use these values in your environment configuration under `env.processor.inverse_kinematics.end_effector_bounds` (see `InverseKinematicsConfig` in `lerobot/envs/configs.py`) **Example Configuration** ```json -"end_effector_bounds": { - "max": [0.24, 0.20, 0.10], - "min": [0.16, -0.08, 0.03] +{ + "env": { + "processor": { + "inverse_kinematics": { + "end_effector_bounds": { + "max": [0.24, 0.2, 0.1], + "min": [0.16, -0.08, 0.03] + } + } + } + } } ``` @@ -404,30 +413,24 @@ We support using a gamepad or a keyboard or the leader arm of the robot. HIL-Serl learns actions in the end-effector space of the robot. Therefore, the teleoperation will control the end-effector's x,y,z displacements. -For that we need to define a version of the robot that takes actions in the end-effector space. Check the robot class `SO100FollowerEndEffector` and its configuration `SO100FollowerEndEffectorConfig` for the default parameters related to the end-effector space. +The end-effector transformation is applied by the processor pipeline (`InverseKinematicsRLStep`, `EEBoundsAndSafety`, `EEReferenceAndDelta`, `GripperVelocityToJoint`) configured under `env.processor.inverse_kinematics` (`InverseKinematicsConfig`) and `env.processor.gripper` / `env.processor.max_gripper_pos`. The defaults related to the end-effector space are: ```python -class SO100FollowerEndEffectorConfig(SO100FollowerConfig): - """Configuration for the SO100FollowerEndEffector robot.""" +class InverseKinematicsConfig: + """Configuration for inverse kinematics processing.""" - # Default bounds for the end-effector position (in meters) - end_effector_bounds: dict[str, list[float]] = field( # bounds for the end-effector in x,y,z direction - default_factory=lambda: { - "min": [-1.0, -1.0, -1.0], # min x, y, z - "max": [1.0, 1.0, 1.0], # max x, y, z - } - ) + urdf_path: str | None = None + target_frame_name: str | None = None + # bounds for the end-effector in x,y,z direction + end_effector_bounds: dict[str, list[float]] | None = None + # maximum step size for the end-effector in x,y,z direction + end_effector_step_sizes: dict[str, float] | None = None - max_gripper_pos: float = 50 # maximum gripper position that the gripper will be open at - - end_effector_step_sizes: dict[str, float] = field( # maximum step size for the end-effector in x,y,z direction - default_factory=lambda: { - "x": 0.02, - "y": 0.02, - "z": 0.02, - } - ) +class HILSerlProcessorConfig: + ... + # maximum gripper position that the gripper will be open at + max_gripper_pos: float | None = 100.0 ``` @@ -606,11 +609,11 @@ This guide explains how to train a reward classifier for human-in-the-loop reinf **Note**: Training a reward classifier is optional. You can start the first round of RL experiments by annotating the success manually with your gamepad or keyboard device. -The reward classifier implementation in `modeling_classifier.py` uses a pretrained vision model to process the images. It can output either a single value for binary rewards to predict success/fail cases or multiple values for multi-class settings. +The reward classifier implementation in `lerobot/rewards/classifier/modeling_classifier.py` uses a pretrained vision model to process the images. It can output either a single value for binary rewards to predict success/fail cases or multiple values for multi-class settings. **Collecting a Dataset for the reward classifier** -Before training, you need to collect a dataset with labeled examples. The `record_dataset` function in `gym_manipulator.py` enables the process of collecting a dataset of observations, actions, and rewards. +Before training, you need to collect a dataset with labeled examples. Setting `mode: "record"` in your config and running `gym_manipulator.py` enables the process of collecting a dataset of observations, actions, and rewards. To collect a dataset, you need to modify some parameters in the environment configuration based on HILSerlRobotEnvConfig. @@ -658,7 +661,7 @@ Example configuration section for data collection: }, "dataset": { "repo_id": "hf_username/dataset_name", - "dataset_root": "data/your_dataset", + "root": "data/your_dataset", "task": "reward_classifier_task", "num_episodes_to_record": 20, "replay_episode": null, @@ -671,7 +674,7 @@ Example configuration section for data collection: **Reward Classifier Configuration** -The reward classifier is configured using `configuration_classifier.py`. Here are the key parameters: +The reward classifier is configured using `lerobot/rewards/classifier/configuration_classifier.py`. Here are the key parameters: - **model_name**: Base model architecture (e.g., we mainly use `"helper2424/resnet10"`) - **model_type**: `"cnn"` or `"transformer"` @@ -689,7 +692,7 @@ Example configuration for training the [reward classifier](https://huggingface.c "repo_id": "hf_username/dataset_name", "root": null }, - "policy": { + "reward_model": { "type": "reward_classifier", "model_name": "helper2424/resnet10", "model_type": "cnn", @@ -699,7 +702,6 @@ Example configuration for training the [reward classifier](https://huggingface.c "dropout_rate": 0.1, "learning_rate": 1e-4, "device": "cuda", - "use_amp": true, "input_features": { "observation.images.front": { "type": "VISUAL", @@ -818,13 +820,14 @@ The LeRobot system uses a distributed actor-learner architecture for training. T **Configuration Setup** -Create a training configuration file (example available [here](https://huggingface.co/datasets/lerobot/config_examples/resolve/main/rl/train_config.json)). The training config is based on the main `TrainRLServerPipelineConfig` class in `lerobot/configs/train.py`. +Create a training configuration file (example available [here](https://huggingface.co/datasets/lerobot/config_examples/resolve/main/rl/train_config.json)). The training config is based on the main `TrainRLServerPipelineConfig` class in `lerobot/rl/train_rl.py`. -1. Configure the policy settings (`type="sac"`, `device`, etc.) -2. Set `dataset` to your cropped dataset -3. Configure environment settings with crop parameters -4. Check the other parameters related to SAC in [configuration_sac.py](https://github.com/huggingface/lerobot/blob/main/src/lerobot/policies/sac/configuration_sac.py#L79). -5. Verify that the `policy` config is correct with the right `input_features` and `output_features` for your task. +1. Configure the policy settings (`type="gaussian_actor"`, `device`, etc.) +2. Configure the algorithm settings under the top-level `algorithm` block (`type="sac"`, learning rates, discount, etc., defined in `lerobot/rl/algorithms/sac/configuration_sac.py`). +3. Set `dataset` to your cropped dataset +4. Configure environment settings with crop parameters +5. Check the other parameters related to the Gaussian Actor in [configuration_gaussian_actor.py](https://github.com/huggingface/lerobot/blob/main/src/lerobot/policies/gaussian_actor/configuration_gaussian_actor.py#L79). +6. Verify that the `policy` config is correct with the right `input_features` and `output_features` for your task. **Starting the Learner** @@ -926,7 +929,7 @@ The ideal behaviour is that your intervention rate should drop gradually during Some configuration values have a disproportionate impact on training stability and speed: -- **`temperature_init`** (`policy.temperature_init`) – initial entropy temperature in SAC. Higher values encourage more exploration; lower values make the policy more deterministic early on. A good starting point is `1e-2`. We observed that setting it too high can make human interventions ineffective and slow down learning. +- **`temperature_init`** (`algorithm.temperature_init`) – initial entropy temperature in SAC. Higher values encourage more exploration; lower values make the policy more deterministic early on. A good starting point is `1e-2`. We observed that setting it too high can make human interventions ineffective and slow down learning. - **`policy_parameters_push_frequency`** (`policy.actor_learner_config.policy_parameters_push_frequency`) – interval in _seconds_ between two weight pushes from the learner to the actor. The default is `4 s`. Decrease to **1-2 s** to provide fresher weights (at the cost of more network traffic); increase only if your connection is slow, as this will reduce sample efficiency. - **`storage_device`** (`policy.storage_device`) – device on which the learner keeps the policy parameters. If you have spare GPU memory, set this to `"cuda"` (instead of the default `"cpu"`). Keeping the weights on-GPU removes CPU→GPU transfer overhead and can significantly increase the number of learner updates per second. diff --git a/examples/tutorial/rl/hilserl_example.py b/examples/tutorial/rl/hilserl_example.py index 71b50e97c..f82c9b048 100644 --- a/examples/tutorial/rl/hilserl_example.py +++ b/examples/tutorial/rl/hilserl_example.py @@ -4,13 +4,13 @@ from pathlib import Path from queue import Empty, Full import torch -import torch.optim as optim from lerobot.datasets import LeRobotDataset from lerobot.envs.configs import HILSerlProcessorConfig, HILSerlRobotEnvConfig -from lerobot.policies import SACConfig -from lerobot.policies.sac.modeling_sac import SACPolicy +from lerobot.policies import GaussianActorConfig +from lerobot.policies.gaussian_actor.modeling_gaussian_actor import GaussianActorPolicy from lerobot.rewards.classifier.modeling_classifier import Classifier +from lerobot.rl.algorithms.sac import SACAlgorithm, SACAlgorithmConfig from lerobot.rl.buffer import ReplayBuffer from lerobot.rl.gym_manipulator import make_robot_env from lerobot.robots.so_follower import SO100FollowerConfig @@ -28,7 +28,7 @@ def run_learner( transitions_queue: mp.Queue, parameters_queue: mp.Queue, shutdown_event: mp.Event, - policy_learner: SACPolicy, + policy_learner: GaussianActorPolicy, online_buffer: ReplayBuffer, offline_buffer: ReplayBuffer, lr: float = 3e-4, @@ -40,8 +40,9 @@ def run_learner( policy_learner.train() policy_learner.to(device) - # Create Adam optimizer from scratch - simple and clean - optimizer = optim.Adam(policy_learner.parameters(), lr=lr) + algo_config = SACAlgorithmConfig.from_policy_config(policy_learner.config) + algorithm = SACAlgorithm(policy=policy_learner, config=algo_config) + algorithm.make_optimizers_and_scheduler() print(f"[LEARNER] Online buffer capacity: {online_buffer.capacity}") print(f"[LEARNER] Offline buffer capacity: {offline_buffer.capacity}") @@ -83,24 +84,26 @@ def run_learner( else: batch[key] = online_batch[key] - loss, _ = policy_learner.forward(batch) + def batch_iter(b=batch): + while True: + yield b - optimizer.zero_grad() - loss.backward() - optimizer.step() + stats = algorithm.update(batch_iter()) training_step += 1 if training_step % LOG_EVERY == 0: + log_dict = stats.to_log_dict() print( - f"[LEARNER] Training step {training_step}, Loss: {loss.item():.4f}, " + f"[LEARNER] Training step {training_step}, " + f"critic_loss: {log_dict.get('critic', 'N/A'):.4f}, " f"Buffers: Online={len(online_buffer)}, Offline={len(offline_buffer)}" ) # Send updated parameters to actor every 10 training steps if training_step % SEND_EVERY == 0: try: - state_dict = {k: v.cpu() for k, v in policy_learner.state_dict().items()} - parameters_queue.put_nowait(state_dict) + weights = algorithm.get_weights() + parameters_queue.put_nowait(weights) print("[LEARNER] Sent updated parameters to actor") except Full: # Missing write due to queue not being consumed (should happen rarely) @@ -113,7 +116,7 @@ def run_actor( transitions_queue: mp.Queue, parameters_queue: mp.Queue, shutdown_event: mp.Event, - policy_actor: SACPolicy, + policy_actor: GaussianActorPolicy, reward_classifier: Classifier, env_cfg: HILSerlRobotEnvConfig, device: torch.device = "mps", @@ -144,15 +147,15 @@ def run_actor( while step < MAX_STEPS_PER_EPISODE and not shutdown_event.is_set(): try: - new_params = parameters_queue.get_nowait() - policy_actor.load_state_dict(new_params) + new_weights = parameters_queue.get_nowait() + policy_actor.load_state_dict(new_weights) print("[ACTOR] Updated policy parameters from learner") except Empty: # No new updated parameters available from learner, waiting pass - # Get action from policy + # Get action from policy (returns full action: continuous + discrete) policy_obs = make_policy_obs(obs, device=device) - action_tensor = policy_actor.select_action(policy_obs) # predicts a single action + action_tensor = policy_actor.select_action(policy_obs) action = action_tensor.squeeze(0).cpu().numpy() # Step environment @@ -261,14 +264,14 @@ def main(): action_features = hw_to_dataset_features(env.robot.action_features, "action") # Create SAC policy for action selection - policy_cfg = SACConfig( + policy_cfg = GaussianActorConfig( device=device, input_features=obs_features, output_features=action_features, ) - policy_actor = SACPolicy(policy_cfg) - policy_learner = SACPolicy(policy_cfg) + policy_actor = GaussianActorPolicy(policy_cfg) + policy_learner = GaussianActorPolicy(policy_cfg) demonstrations_repo_id = "lerobot/example_hil_serl_dataset" offline_dataset = LeRobotDataset(repo_id=demonstrations_repo_id) diff --git a/pyproject.toml b/pyproject.toml index 4deb34034..870f7b62b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -195,7 +195,7 @@ groot = [ sarm = ["lerobot[transformers-dep]", "pydantic>=2.0.0,<3.0.0", "faker>=33.0.0,<35.0.0", "lerobot[matplotlib-dep]", "lerobot[qwen-vl-utils-dep]"] xvla = ["lerobot[transformers-dep]"] eo1 = ["lerobot[transformers-dep]", "lerobot[qwen-vl-utils-dep]"] -hilserl = ["lerobot[transformers-dep]", "gym-hil>=0.1.13,<0.2.0", "lerobot[grpcio-dep]", "lerobot[placo-dep]"] +hilserl = ["lerobot[transformers-dep]", "lerobot[dataset]", "gym-hil>=0.1.13,<0.2.0", "lerobot[grpcio-dep]", "lerobot[placo-dep]"] # Features async = ["lerobot[grpcio-dep]", "lerobot[matplotlib-dep]"] diff --git a/src/lerobot/common/train_utils.py b/src/lerobot/common/train_utils.py index 3e96e1330..21ee514de 100644 --- a/src/lerobot/common/train_utils.py +++ b/src/lerobot/common/train_utils.py @@ -99,6 +99,7 @@ def save_checkpoint( optimizer (Optimizer | None, optional): The optimizer to save the state from. Defaults to None. scheduler (LRScheduler | None, optional): The scheduler to save the state from. Defaults to None. preprocessor: The preprocessor/pipeline to save. Defaults to None. + postprocessor: The postprocessor/pipeline to save. Defaults to None. """ pretrained_dir = checkpoint_dir / PRETRAINED_MODEL_DIR policy.save_pretrained(pretrained_dir) diff --git a/src/lerobot/configs/train.py b/src/lerobot/configs/train.py index 318821166..388de9437 100644 --- a/src/lerobot/configs/train.py +++ b/src/lerobot/configs/train.py @@ -269,10 +269,3 @@ class TrainPipelineConfig(HubMixin): with draccus.config_type("json"): return draccus.parse(cls, config_file, args=cli_args) - - -@dataclass(kw_only=True) -class TrainRLServerPipelineConfig(TrainPipelineConfig): - # NOTE: In RL, we don't need an offline dataset - # TODO: Make `TrainPipelineConfig.dataset` optional - dataset: DatasetConfig | None = None # type: ignore[assignment] # because the parent class has made it's type non-optional diff --git a/src/lerobot/policies/__init__.py b/src/lerobot/policies/__init__.py index 2633d04ad..3a6b8e5d2 100644 --- a/src/lerobot/policies/__init__.py +++ b/src/lerobot/policies/__init__.py @@ -18,13 +18,13 @@ from .act.configuration_act import ACTConfig as ACTConfig from .diffusion.configuration_diffusion import DiffusionConfig as DiffusionConfig from .eo1.configuration_eo1 import EO1Config as EO1Config from .factory import get_policy_class, make_policy, make_policy_config, make_pre_post_processors +from .gaussian_actor.configuration_gaussian_actor import GaussianActorConfig as GaussianActorConfig from .groot.configuration_groot import GrootConfig as GrootConfig from .multi_task_dit.configuration_multi_task_dit import MultiTaskDiTConfig as MultiTaskDiTConfig from .pi0.configuration_pi0 import PI0Config as PI0Config from .pi0_fast.configuration_pi0_fast import PI0FastConfig as PI0FastConfig from .pi05.configuration_pi05 import PI05Config as PI05Config from .pretrained import PreTrainedPolicy as PreTrainedPolicy -from .sac.configuration_sac import SACConfig as SACConfig from .smolvla.configuration_smolvla import SmolVLAConfig as SmolVLAConfig from .tdmpc.configuration_tdmpc import TDMPCConfig as TDMPCConfig from .utils import make_robot_action, prepare_observation_for_inference @@ -32,21 +32,21 @@ from .vqbet.configuration_vqbet import VQBeTConfig as VQBeTConfig from .wall_x.configuration_wall_x import WallXConfig as WallXConfig from .xvla.configuration_xvla import XVLAConfig as XVLAConfig -# NOTE: Policy modeling classes (e.g., SACPolicy) are intentionally NOT re-exported here. +# NOTE: Policy modeling classes (e.g., GaussianActorPolicy) are intentionally NOT re-exported here. # They have heavy optional dependencies and are loaded lazily via get_policy_class(). -# Import directly: ``from lerobot.policies.sac.modeling_sac import SACPolicy`` +# Import directly: ``from lerobot.policies.gaussian_actor.modeling_gaussian_actor import GaussianActorPolicy`` __all__ = [ # Configuration classes "ACTConfig", "DiffusionConfig", + "EO1Config", + "GaussianActorConfig", "GrootConfig", "MultiTaskDiTConfig", - "EO1Config", "PI0Config", "PI0FastConfig", "PI05Config", - "SACConfig", "SmolVLAConfig", "TDMPCConfig", "VQBeTConfig", diff --git a/src/lerobot/policies/factory.py b/src/lerobot/policies/factory.py index 3609cc7c3..8937bc6ae 100644 --- a/src/lerobot/policies/factory.py +++ b/src/lerobot/policies/factory.py @@ -47,12 +47,12 @@ from lerobot.utils.feature_utils import dataset_to_policy_features from .act.configuration_act import ACTConfig from .diffusion.configuration_diffusion import DiffusionConfig from .eo1.configuration_eo1 import EO1Config +from .gaussian_actor.configuration_gaussian_actor import GaussianActorConfig from .groot.configuration_groot import GrootConfig from .multi_task_dit.configuration_multi_task_dit import MultiTaskDiTConfig from .pi0.configuration_pi0 import PI0Config from .pi05.configuration_pi05 import PI05Config from .pretrained import PreTrainedPolicy -from .sac.configuration_sac import SACConfig from .smolvla.configuration_smolvla import SmolVLAConfig from .tdmpc.configuration_tdmpc import TDMPCConfig from .utils import validate_visual_features_consistency @@ -88,7 +88,7 @@ def get_policy_class(name: str) -> type[PreTrainedPolicy]: Args: name: The name of the policy. Supported names are "tdmpc", "diffusion", "act", - "multi_task_dit", "vqbet", "pi0", "pi05", "sac", "smolvla", "wall_x". + "multi_task_dit", "vqbet", "pi0", "pi05", "gaussian_actor", "smolvla", "wall_x". Returns: The policy class corresponding to the given name. @@ -127,10 +127,10 @@ def get_policy_class(name: str) -> type[PreTrainedPolicy]: from .pi05.modeling_pi05 import PI05Policy return PI05Policy - elif name == "sac": - from .sac.modeling_sac import SACPolicy + elif name == "gaussian_actor": + from .gaussian_actor.modeling_gaussian_actor import GaussianActorPolicy - return SACPolicy + return GaussianActorPolicy elif name == "smolvla": from .smolvla.modeling_smolvla import SmolVLAPolicy @@ -167,7 +167,7 @@ def make_policy_config(policy_type: str, **kwargs) -> PreTrainedConfig: Args: policy_type: The type of the policy. Supported types include "tdmpc", - "multi_task_dit", "diffusion", "act", "vqbet", "pi0", "pi05", "sac", + "multi_task_dit", "diffusion", "act", "vqbet", "pi0", "pi05", "gaussian_actor", "smolvla", "wall_x". **kwargs: Keyword arguments to be passed to the configuration class constructor. @@ -191,8 +191,8 @@ def make_policy_config(policy_type: str, **kwargs) -> PreTrainedConfig: return PI0Config(**kwargs) elif policy_type == "pi05": return PI05Config(**kwargs) - elif policy_type == "sac": - return SACConfig(**kwargs) + elif policy_type == "gaussian_actor": + return GaussianActorConfig(**kwargs) elif policy_type == "smolvla": return SmolVLAConfig(**kwargs) elif policy_type == "groot": @@ -365,10 +365,10 @@ def make_pre_post_processors( dataset_stats=kwargs.get("dataset_stats"), ) - elif isinstance(policy_cfg, SACConfig): - from .sac.processor_sac import make_sac_pre_post_processors + elif isinstance(policy_cfg, GaussianActorConfig): + from .gaussian_actor.processor_gaussian_actor import make_gaussian_actor_pre_post_processors - processors = make_sac_pre_post_processors( + processors = make_gaussian_actor_pre_post_processors( config=policy_cfg, dataset_stats=kwargs.get("dataset_stats"), ) diff --git a/src/lerobot/policies/sac/__init__.py b/src/lerobot/policies/gaussian_actor/__init__.py similarity index 67% rename from src/lerobot/policies/sac/__init__.py rename to src/lerobot/policies/gaussian_actor/__init__.py index cf5f149f3..c3c5855ac 100644 --- a/src/lerobot/policies/sac/__init__.py +++ b/src/lerobot/policies/gaussian_actor/__init__.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .configuration_sac import SACConfig -from .modeling_sac import SACPolicy -from .processor_sac import make_sac_pre_post_processors +from .configuration_gaussian_actor import GaussianActorConfig +from .modeling_gaussian_actor import GaussianActorPolicy +from .processor_gaussian_actor import make_gaussian_actor_pre_post_processors -__all__ = ["SACConfig", "SACPolicy", "make_sac_pre_post_processors"] +__all__ = ["GaussianActorConfig", "GaussianActorPolicy", "make_gaussian_actor_pre_post_processors"] diff --git a/src/lerobot/policies/sac/configuration_sac.py b/src/lerobot/policies/gaussian_actor/configuration_gaussian_actor.py similarity index 73% rename from src/lerobot/policies/sac/configuration_sac.py rename to src/lerobot/policies/gaussian_actor/configuration_gaussian_actor.py index db0a77672..e51653992 100644 --- a/src/lerobot/policies/sac/configuration_sac.py +++ b/src/lerobot/policies/gaussian_actor/configuration_gaussian_actor.py @@ -1,4 +1,4 @@ -# !/usr/bin/env python +#!/usr/bin/env python # Copyright 2025 The HuggingFace Inc. team. # All rights reserved. @@ -75,18 +75,19 @@ class PolicyConfig: init_final: float = 0.05 -@PreTrainedConfig.register_subclass("sac") +@PreTrainedConfig.register_subclass("gaussian_actor") @dataclass -class SACConfig(PreTrainedConfig): - """Soft Actor-Critic (SAC) configuration. +class GaussianActorConfig(PreTrainedConfig): + """Gaussian actor configuration. - SAC is an off-policy actor-critic deep RL algorithm based on the maximum entropy - reinforcement learning framework. It learns a policy and a Q-function simultaneously - using experience collected from the environment. + This configures the policy-side (actor + observation encoder) of a Gaussian + policy, as used by SAC and related maximum-entropy continuous-control algorithms. + By default the actor output is a tanh-squashed diagonal Gaussian + (``TanhMultivariateNormalDiag``); the tanh squashing can be disabled via + ``policy_kwargs.use_tanh_squash``. The critics, temperature, and Bellman-update + logic live on the algorithm side (see ``lerobot.rl.algorithms.sac``). - This configuration class contains all the parameters needed to define a SAC agent, - including network architectures, optimization settings, and algorithm-specific - hyperparameters. + CLI: ``--policy.type=gaussian_actor``. """ # Mapping of feature types to normalization modes @@ -122,7 +123,7 @@ class SACConfig(PreTrainedConfig): device: str = "cpu" # Device to store the model on storage_device: str = "cpu" - # Name of the vision encoder model (Set to "helper2424/resnet10" for hil serl resnet10) + # Name of the vision encoder model (Set to "lerobot/resnet10" for hil serl resnet10) vision_encoder_name: str | None = None # Whether to freeze the vision encoder during training freeze_vision_encoder: bool = True @@ -135,7 +136,13 @@ class SACConfig(PreTrainedConfig): # Dimension of the image embedding pooling image_embedding_pooling_dim: int = 8 - # Training parameter + # Encoder architecture + # Hidden dimension size for the state encoder + state_encoder_hidden_dim: int = 256 + # Dimension of the latent space + latent_dim: int = 256 + + # Online training (TODO(Khalil): relocate to TrainRLServerPipelineConfig) # Number of steps for online training online_steps: int = 1000000 # Capacity of the online replay buffer @@ -146,67 +153,38 @@ class SACConfig(PreTrainedConfig): async_prefetch: bool = False # Number of steps before learning starts online_step_before_learning: int = 100 - # Frequency of policy updates - policy_update_freq: int = 1 - # SAC algorithm parameters - # Discount factor for the SAC algorithm - discount: float = 0.99 - # Initial temperature value - temperature_init: float = 1.0 - # Number of critics in the ensemble - num_critics: int = 2 - # Number of subsampled critics for training - num_subsample_critics: int | None = None - # Learning rate for the critic network - critic_lr: float = 3e-4 - # Learning rate for the actor network - actor_lr: float = 3e-4 - # Learning rate for the temperature parameter - temperature_lr: float = 3e-4 - # Weight for the critic target update - critic_target_update_weight: float = 0.005 - # Update-to-data ratio for the UTD algorithm (If you want enable utd_ratio, you need to set it to >1) - utd_ratio: int = 1 - # Hidden dimension size for the state encoder - state_encoder_hidden_dim: int = 256 - # Dimension of the latent space - latent_dim: int = 256 - # Target entropy for the SAC algorithm - target_entropy: float | None = None - # Whether to use backup entropy for the SAC algorithm - use_backup_entropy: bool = True - # Gradient clipping norm for the SAC algorithm - grad_clip_norm: float = 40.0 - - # Network configuration - # Configuration for the critic network architecture - critic_network_kwargs: CriticNetworkConfig = field(default_factory=CriticNetworkConfig) - # Configuration for the actor network architecture - actor_network_kwargs: ActorNetworkConfig = field(default_factory=ActorNetworkConfig) - # Configuration for the policy parameters - policy_kwargs: PolicyConfig = field(default_factory=PolicyConfig) - # Configuration for the discrete critic network - discrete_critic_network_kwargs: CriticNetworkConfig = field(default_factory=CriticNetworkConfig) + # Actor-learner transport (TODO(Khalil): relocate to TrainRLServerPipelineConfig). # Configuration for actor-learner architecture actor_learner_config: ActorLearnerConfig = field(default_factory=ActorLearnerConfig) # Configuration for concurrency settings (you can use threads or processes for the actor and learner) concurrency: ConcurrencyConfig = field(default_factory=ConcurrencyConfig) - # Optimizations - use_torch_compile: bool = True + # Network architecture + # Configuration for the actor network architecture + actor_network_kwargs: ActorNetworkConfig = field(default_factory=ActorNetworkConfig) + # Configuration for the policy parameters (Gaussian head) + policy_kwargs: PolicyConfig = field(default_factory=PolicyConfig) + # Configuration for the discrete critic network + discrete_critic_network_kwargs: CriticNetworkConfig = field(default_factory=CriticNetworkConfig) def __post_init__(self): super().__post_init__() - # Any validation specific to SAC configuration + # Any validation specific to GaussianActor configuration def get_optimizer_preset(self) -> MultiAdamConfig: + # Default learning rate used to satisfy the abstract ``get_optimizer_preset()`` + # contract from ``PreTrainedConfig``. The actual optimizers used during RL + # training are built by ``SACAlgorithm.make_optimizers_and_scheduler()`` from + # ``SACAlgorithmConfig.{actor_lr,critic_lr,temperature_lr}`` and fully bypass + # this preset. + default_lr = 3e-4 return MultiAdamConfig( weight_decay=0.0, optimizer_groups={ - "actor": {"lr": self.actor_lr}, - "critic": {"lr": self.critic_lr}, - "temperature": {"lr": self.temperature_lr}, + "actor": {"lr": default_lr}, + "critic": {"lr": default_lr}, + "temperature": {"lr": default_lr}, }, ) diff --git a/src/lerobot/policies/sac/modeling_sac.py b/src/lerobot/policies/gaussian_actor/modeling_gaussian_actor.py similarity index 54% rename from src/lerobot/policies/sac/modeling_sac.py rename to src/lerobot/policies/gaussian_actor/modeling_gaussian_actor.py index cc7030ce2..a833d01cc 100644 --- a/src/lerobot/policies/sac/modeling_sac.py +++ b/src/lerobot/policies/gaussian_actor/modeling_gaussian_actor.py @@ -15,16 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import math from collections.abc import Callable from dataclasses import asdict -from typing import Literal -import einops -import numpy as np import torch import torch.nn as nn -import torch.nn.functional as F # noqa: N812 from torch import Tensor from torch.distributions import MultivariateNormal, TanhTransform, Transform, TransformedDistribution @@ -32,20 +27,20 @@ from lerobot.utils.constants import ACTION, OBS_ENV_STATE, OBS_STATE from ..pretrained import PreTrainedPolicy from ..utils import get_device_from_parameters -from .configuration_sac import SACConfig, is_image_feature +from .configuration_gaussian_actor import GaussianActorConfig, is_image_feature DISCRETE_DIMENSION_INDEX = -1 # Gripper is always the last dimension -class SACPolicy( +class GaussianActorPolicy( PreTrainedPolicy, ): - config_class = SACConfig - name = "sac" + config_class = GaussianActorConfig + name = "gaussian_actor" def __init__( self, - config: SACConfig | None = None, + config: GaussianActorConfig | None = None, ): super().__init__(config) config.validate_features() @@ -54,9 +49,8 @@ class SACPolicy( # Determine action dimension and initialize all components continuous_action_dim = config.output_features[ACTION].shape[0] self._init_encoders() - self._init_critics(continuous_action_dim) self._init_actor(continuous_action_dim) - self._init_temperature() + self._init_discrete_critic() def get_optim_params(self) -> dict: optim_params = { @@ -65,11 +59,7 @@ class SACPolicy( for n, p in self.actor.named_parameters() if not n.startswith("encoder") or not self.shared_encoder ], - "critic": self.critic_ensemble.parameters(), - "temperature": self.log_alpha, } - if self.config.num_discrete_actions is not None: - optim_params["discrete_critic"] = self.discrete_critic.parameters() return optim_params def reset(self): @@ -79,7 +69,9 @@ class SACPolicy( @torch.no_grad() def predict_action_chunk(self, batch: dict[str, Tensor]) -> Tensor: """Predict a chunk of actions given environment observations.""" - raise NotImplementedError("SACPolicy does not support action chunking. It returns single actions!") + raise NotImplementedError( + "GaussianActorPolicy does not support action chunking. It returns single actions!" + ) @torch.no_grad() def select_action(self, batch: dict[str, Tensor]) -> Tensor: @@ -92,360 +84,43 @@ class SACPolicy( actions, _, _ = self.actor(batch, observations_features) if self.config.num_discrete_actions is not None: - discrete_action_value = self.discrete_critic(batch, observations_features) - discrete_action = torch.argmax(discrete_action_value, dim=-1, keepdim=True) + if self.discrete_critic is not None: + discrete_action_value = self.discrete_critic(batch, observations_features) + discrete_action = torch.argmax(discrete_action_value, dim=-1, keepdim=True) + else: + discrete_action = torch.ones( + (*actions.shape[:-1], 1), device=actions.device, dtype=actions.dtype + ) actions = torch.cat([actions, discrete_action], dim=-1) return actions - def critic_forward( - self, - observations: dict[str, Tensor], - actions: Tensor, - use_target: bool = False, - observation_features: Tensor | None = None, - ) -> Tensor: - """Forward pass through a critic network ensemble + def forward(self, batch: dict[str, Tensor | dict[str, Tensor]]) -> dict[str, Tensor]: + """Actor forward pass: sample actions and return log-probabilities. Args: - observations: Dictionary of observations - actions: Action tensor - use_target: If True, use target critics, otherwise use ensemble critics + batch: A flat observation dict, or a training dict containing + ``"state"`` (observations) and optionally ``"observation_feature"`` + (pre-computed encoder features). Returns: - Tensor of Q-values from all critics + Dict with ``"action"``, ``"log_prob"``, and ``"action_mean"`` tensors. """ - - critics = self.critic_target if use_target else self.critic_ensemble - q_values = critics(observations, actions, observation_features) - return q_values - - def discrete_critic_forward( - self, observations, use_target=False, observation_features=None - ) -> torch.Tensor: - """Forward pass through a discrete critic network - - Args: - observations: Dictionary of observations - use_target: If True, use target critics, otherwise use ensemble critics - observation_features: Optional pre-computed observation features to avoid recomputing encoder output - - Returns: - Tensor of Q-values from the discrete critic network - """ - discrete_critic = self.discrete_critic_target if use_target else self.discrete_critic - q_values = discrete_critic(observations, observation_features) - return q_values - - def forward( - self, - batch: dict[str, Tensor | dict[str, Tensor]], - model: Literal["actor", "critic", "temperature", "discrete_critic"] = "critic", - ) -> dict[str, Tensor]: - """Compute the loss for the given model - - Args: - batch: Dictionary containing: - - action: Action tensor - - reward: Reward tensor - - state: Observations tensor dict - - next_state: Next observations tensor dict - - done: Done mask tensor - - observation_feature: Optional pre-computed observation features - - next_observation_feature: Optional pre-computed next observation features - model: Which model to compute the loss for ("actor", "critic", "discrete_critic", or "temperature") - - Returns: - The computed loss tensor - """ - # Extract common components from batch - actions: Tensor = batch[ACTION] - observations: dict[str, Tensor] = batch["state"] - observation_features: Tensor = batch.get("observation_feature") - - if model == "critic": - # Extract critic-specific components - rewards: Tensor = batch["reward"] - next_observations: dict[str, Tensor] = batch["next_state"] - done: Tensor = batch["done"] - next_observation_features: Tensor = batch.get("next_observation_feature") - - loss_critic = self.compute_loss_critic( - observations=observations, - actions=actions, - rewards=rewards, - next_observations=next_observations, - done=done, - observation_features=observation_features, - next_observation_features=next_observation_features, - ) - - return {"loss_critic": loss_critic} - - if model == "discrete_critic" and self.config.num_discrete_actions is not None: - # Extract critic-specific components - rewards: Tensor = batch["reward"] - next_observations: dict[str, Tensor] = batch["next_state"] - done: Tensor = batch["done"] - next_observation_features: Tensor = batch.get("next_observation_feature") - complementary_info = batch.get("complementary_info") - loss_discrete_critic = self.compute_loss_discrete_critic( - observations=observations, - actions=actions, - rewards=rewards, - next_observations=next_observations, - done=done, - observation_features=observation_features, - next_observation_features=next_observation_features, - complementary_info=complementary_info, - ) - return {"loss_discrete_critic": loss_discrete_critic} - if model == "actor": - return { - "loss_actor": self.compute_loss_actor( - observations=observations, - observation_features=observation_features, - ) - } - - if model == "temperature": - return { - "loss_temperature": self.compute_loss_temperature( - observations=observations, - observation_features=observation_features, - ) - } - - raise ValueError(f"Unknown model type: {model}") - - def update_target_networks(self): - """Update target networks with exponential moving average""" - for target_param, param in zip( - self.critic_target.parameters(), - self.critic_ensemble.parameters(), - strict=True, - ): - target_param.data.copy_( - param.data * self.config.critic_target_update_weight - + target_param.data * (1.0 - self.config.critic_target_update_weight) - ) - if self.config.num_discrete_actions is not None: - for target_param, param in zip( - self.discrete_critic_target.parameters(), - self.discrete_critic.parameters(), - strict=True, - ): - target_param.data.copy_( - param.data * self.config.critic_target_update_weight - + target_param.data * (1.0 - self.config.critic_target_update_weight) - ) - - @property - def temperature(self) -> float: - """Return the current temperature value, always in sync with log_alpha.""" - return self.log_alpha.exp().item() - - def compute_loss_critic( - self, - observations, - actions, - rewards, - next_observations, - done, - observation_features: Tensor | None = None, - next_observation_features: Tensor | None = None, - ) -> Tensor: - with torch.no_grad(): - next_action_preds, next_log_probs, _ = self.actor(next_observations, next_observation_features) - - # 2- compute q targets - q_targets = self.critic_forward( - observations=next_observations, - actions=next_action_preds, - use_target=True, - observation_features=next_observation_features, - ) - - # subsample critics to prevent overfitting if use high UTD (update to date) - # TODO: Get indices before forward pass to avoid unnecessary computation - if self.config.num_subsample_critics is not None: - indices = torch.randperm(self.config.num_critics) - indices = indices[: self.config.num_subsample_critics] - q_targets = q_targets[indices] - - # critics subsample size - min_q, _ = q_targets.min(dim=0) # Get values from min operation - if self.config.use_backup_entropy: - min_q = min_q - (self.temperature * next_log_probs) - - td_target = rewards + (1 - done) * self.config.discount * min_q - - # 3- compute predicted qs - if self.config.num_discrete_actions is not None: - # NOTE: We only want to keep the continuous action part - # In the buffer we have the full action space (continuous + discrete) - # We need to split them before concatenating them in the critic forward - actions: Tensor = actions[:, :DISCRETE_DIMENSION_INDEX] - q_preds = self.critic_forward( - observations=observations, - actions=actions, - use_target=False, - observation_features=observation_features, - ) - - # 4- Calculate loss - # Compute state-action value loss (TD loss) for all of the Q functions in the ensemble. - td_target_duplicate = einops.repeat(td_target, "b -> e b", e=q_preds.shape[0]) - # You compute the mean loss of the batch for each critic and then to compute the final loss you sum them up - critics_loss = ( - F.mse_loss( - input=q_preds, - target=td_target_duplicate, - reduction="none", - ).mean(dim=1) - ).sum() - return critics_loss - - def compute_loss_discrete_critic( - self, - observations, - actions, - rewards, - next_observations, - done, - observation_features=None, - next_observation_features=None, - complementary_info=None, - ): - # NOTE: We only want to keep the discrete action part - # In the buffer we have the full action space (continuous + discrete) - # We need to split them before concatenating them in the critic forward - actions_discrete: Tensor = actions[:, DISCRETE_DIMENSION_INDEX:].clone() - actions_discrete = torch.round(actions_discrete) - actions_discrete = actions_discrete.long() - - discrete_penalties: Tensor | None = None - if complementary_info is not None: - discrete_penalties: Tensor | None = complementary_info.get("discrete_penalty") - - with torch.no_grad(): - # For DQN, select actions using online network, evaluate with target network - next_discrete_qs = self.discrete_critic_forward( - next_observations, use_target=False, observation_features=next_observation_features - ) - best_next_discrete_action = torch.argmax(next_discrete_qs, dim=-1, keepdim=True) - - # Get target Q-values from target network - target_next_discrete_qs = self.discrete_critic_forward( - observations=next_observations, - use_target=True, - observation_features=next_observation_features, - ) - - # Use gather to select Q-values for best actions - target_next_discrete_q = torch.gather( - target_next_discrete_qs, dim=1, index=best_next_discrete_action - ).squeeze(-1) - - # Compute target Q-value with Bellman equation - rewards_discrete = rewards - if discrete_penalties is not None: - rewards_discrete = rewards + discrete_penalties - target_discrete_q = rewards_discrete + (1 - done) * self.config.discount * target_next_discrete_q - - # Get predicted Q-values for current observations - predicted_discrete_qs = self.discrete_critic_forward( - observations=observations, use_target=False, observation_features=observation_features - ) - - # Use gather to select Q-values for taken actions - predicted_discrete_q = torch.gather(predicted_discrete_qs, dim=1, index=actions_discrete).squeeze(-1) - - # Compute MSE loss between predicted and target Q-values - discrete_critic_loss = F.mse_loss(input=predicted_discrete_q, target=target_discrete_q) - return discrete_critic_loss - - def compute_loss_temperature(self, observations, observation_features: Tensor | None = None) -> Tensor: - """Compute the temperature loss""" - # calculate temperature loss - with torch.no_grad(): - _, log_probs, _ = self.actor(observations, observation_features) - temperature_loss = (-self.log_alpha.exp() * (log_probs + self.target_entropy)).mean() - return temperature_loss - - def compute_loss_actor( - self, - observations, - observation_features: Tensor | None = None, - ) -> Tensor: - actions_pi, log_probs, _ = self.actor(observations, observation_features) - - q_preds = self.critic_forward( - observations=observations, - actions=actions_pi, - use_target=False, - observation_features=observation_features, - ) - min_q_preds = q_preds.min(dim=0)[0] - - actor_loss = ((self.temperature * log_probs) - min_q_preds).mean() - return actor_loss + observations = batch.get("state", batch) + observation_features = batch.get("observation_feature") if isinstance(batch, dict) else None + actions, log_probs, means = self.actor(observations, observation_features) + return {"action": actions, "log_prob": log_probs, "action_mean": means} def _init_encoders(self): """Initialize shared or separate encoders for actor and critic.""" self.shared_encoder = self.config.shared_encoder - self.encoder_critic = SACObservationEncoder(self.config) + self.encoder_critic = GaussianActorObservationEncoder(self.config) self.encoder_actor = ( - self.encoder_critic if self.shared_encoder else SACObservationEncoder(self.config) + self.encoder_critic if self.shared_encoder else GaussianActorObservationEncoder(self.config) ) - def _init_critics(self, continuous_action_dim): - """Build critic ensemble, targets, and optional discrete critic.""" - heads = [ - CriticHead( - input_dim=self.encoder_critic.output_dim + continuous_action_dim, - **asdict(self.config.critic_network_kwargs), - ) - for _ in range(self.config.num_critics) - ] - self.critic_ensemble = CriticEnsemble(encoder=self.encoder_critic, ensemble=heads) - target_heads = [ - CriticHead( - input_dim=self.encoder_critic.output_dim + continuous_action_dim, - **asdict(self.config.critic_network_kwargs), - ) - for _ in range(self.config.num_critics) - ] - self.critic_target = CriticEnsemble(encoder=self.encoder_critic, ensemble=target_heads) - self.critic_target.load_state_dict(self.critic_ensemble.state_dict()) - - if self.config.use_torch_compile: - self.critic_ensemble = torch.compile(self.critic_ensemble) - self.critic_target = torch.compile(self.critic_target) - - if self.config.num_discrete_actions is not None: - self._init_discrete_critics() - - def _init_discrete_critics(self): - """Build discrete discrete critic ensemble and target networks.""" - self.discrete_critic = DiscreteCritic( - encoder=self.encoder_critic, - input_dim=self.encoder_critic.output_dim, - output_dim=self.config.num_discrete_actions, - **asdict(self.config.discrete_critic_network_kwargs), - ) - self.discrete_critic_target = DiscreteCritic( - encoder=self.encoder_critic, - input_dim=self.encoder_critic.output_dim, - output_dim=self.config.num_discrete_actions, - **asdict(self.config.discrete_critic_network_kwargs), - ) - - # TODO: (maractingi, azouitine) Compile the discrete critic - self.discrete_critic_target.load_state_dict(self.discrete_critic.state_dict()) - def _init_actor(self, continuous_action_dim): - """Initialize policy actor network and default target entropy.""" + """Initialize policy actor network.""" # NOTE: The actor select only the continuous action part self.actor = Policy( encoder=self.encoder_actor, @@ -455,21 +130,25 @@ class SACPolicy( **asdict(self.config.policy_kwargs), ) - self.target_entropy = self.config.target_entropy - if self.target_entropy is None: - dim = continuous_action_dim + (1 if self.config.num_discrete_actions is not None else 0) - self.target_entropy = -np.prod(dim) / 2 + def _init_discrete_critic(self) -> None: + """Initialize discrete critic network.""" + if self.config.num_discrete_actions is None: + self.discrete_critic = None + return - def _init_temperature(self) -> None: - """Set up temperature parameter (log_alpha).""" - temp_init = self.config.temperature_init - self.log_alpha = nn.Parameter(torch.tensor([math.log(temp_init)])) + # TODO(Khalil): Compile the discrete critic + self.discrete_critic = DiscreteCritic( + encoder=self.encoder_critic, + input_dim=self.encoder_critic.output_dim, + output_dim=self.config.num_discrete_actions, + **asdict(self.config.discrete_critic_network_kwargs), + ) -class SACObservationEncoder(nn.Module): +class GaussianActorObservationEncoder(nn.Module): """Encode image and/or state vector observations.""" - def __init__(self, config: SACConfig) -> None: + def __init__(self, config: GaussianActorConfig) -> None: super().__init__() self.config = config self._init_image_layers() @@ -677,84 +356,6 @@ class MLP(nn.Module): return self.net(x) -class CriticHead(nn.Module): - def __init__( - self, - input_dim: int, - hidden_dims: list[int], - activations: Callable[[torch.Tensor], torch.Tensor] | str = nn.SiLU(), - activate_final: bool = False, - dropout_rate: float | None = None, - init_final: float | None = None, - final_activation: Callable[[torch.Tensor], torch.Tensor] | str | None = None, - ): - super().__init__() - self.net = MLP( - input_dim=input_dim, - hidden_dims=hidden_dims, - activations=activations, - activate_final=activate_final, - dropout_rate=dropout_rate, - final_activation=final_activation, - ) - self.output_layer = nn.Linear(in_features=hidden_dims[-1], out_features=1) - if init_final is not None: - nn.init.uniform_(self.output_layer.weight, -init_final, init_final) - nn.init.uniform_(self.output_layer.bias, -init_final, init_final) - else: - orthogonal_init()(self.output_layer.weight) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - return self.output_layer(self.net(x)) - - -class CriticEnsemble(nn.Module): - """ - CriticEnsemble wraps multiple CriticHead modules into an ensemble. - - Args: - encoder (SACObservationEncoder): encoder for observations. - ensemble (List[CriticHead]): list of critic heads. - init_final (float | None): optional initializer scale for final layers. - - Forward returns a tensor of shape (num_critics, batch_size) containing Q-values. - """ - - def __init__( - self, - encoder: SACObservationEncoder, - ensemble: list[CriticHead], - init_final: float | None = None, - ): - super().__init__() - self.encoder = encoder - self.init_final = init_final - self.critics = nn.ModuleList(ensemble) - - def forward( - self, - observations: dict[str, torch.Tensor], - actions: torch.Tensor, - observation_features: torch.Tensor | None = None, - ) -> torch.Tensor: - device = get_device_from_parameters(self) - # Move each tensor in observations to device - observations = {k: v.to(device) for k, v in observations.items()} - - obs_enc = self.encoder(observations, cache=observation_features) - - inputs = torch.cat([obs_enc, actions], dim=-1) - - # Loop through critics and collect outputs - q_values = [] - for critic in self.critics: - q_values.append(critic(inputs)) - - # Stack outputs to match expected shape [num_critics, batch_size] - q_values = torch.stack([q.squeeze(-1) for q in q_values], dim=0) - return q_values - - class DiscreteCritic(nn.Module): def __init__( self, @@ -800,7 +401,7 @@ class DiscreteCritic(nn.Module): class Policy(nn.Module): def __init__( self, - encoder: SACObservationEncoder, + encoder: GaussianActorObservationEncoder, network: nn.Module, action_dim: int, std_min: float = -5, @@ -811,7 +412,7 @@ class Policy(nn.Module): encoder_is_shared: bool = False, ): super().__init__() - self.encoder: SACObservationEncoder = encoder + self.encoder: GaussianActorObservationEncoder = encoder self.network = network self.action_dim = action_dim self.std_min = std_min @@ -885,7 +486,7 @@ class Policy(nn.Module): class DefaultImageEncoder(nn.Module): - def __init__(self, config: SACConfig): + def __init__(self, config: GaussianActorConfig): super().__init__() image_key = next(key for key in config.input_features if is_image_feature(key)) self.image_enc_layers = nn.Sequential( @@ -931,12 +532,12 @@ def freeze_image_encoder(image_encoder: nn.Module): class PretrainedImageEncoder(nn.Module): - def __init__(self, config: SACConfig): + def __init__(self, config: GaussianActorConfig): super().__init__() self.image_enc_layers, self.image_enc_out_shape = self._load_pretrained_vision_encoder(config) - def _load_pretrained_vision_encoder(self, config: SACConfig): + def _load_pretrained_vision_encoder(self, config: GaussianActorConfig): """Set up CNN encoder""" from transformers import AutoModel diff --git a/src/lerobot/policies/sac/processor_sac.py b/src/lerobot/policies/gaussian_actor/processor_gaussian_actor.py similarity index 92% rename from src/lerobot/policies/sac/processor_sac.py rename to src/lerobot/policies/gaussian_actor/processor_gaussian_actor.py index 3409307c2..1e930d178 100644 --- a/src/lerobot/policies/sac/processor_sac.py +++ b/src/lerobot/policies/gaussian_actor/processor_gaussian_actor.py @@ -32,18 +32,18 @@ from lerobot.processor import ( ) from lerobot.utils.constants import POLICY_POSTPROCESSOR_DEFAULT_NAME, POLICY_PREPROCESSOR_DEFAULT_NAME -from .configuration_sac import SACConfig +from .configuration_gaussian_actor import GaussianActorConfig -def make_sac_pre_post_processors( - config: SACConfig, +def make_gaussian_actor_pre_post_processors( + config: GaussianActorConfig, dataset_stats: dict[str, dict[str, torch.Tensor]] | None = None, ) -> tuple[ PolicyProcessorPipeline[dict[str, Any], dict[str, Any]], PolicyProcessorPipeline[PolicyAction, PolicyAction], ]: """ - Constructs pre-processor and post-processor pipelines for the SAC policy. + Constructs pre-processor and post-processor pipelines for the Gaussian actor policy. The pre-processing pipeline prepares input data for the model by: 1. Renaming features to match pretrained configurations. @@ -56,7 +56,7 @@ def make_sac_pre_post_processors( 2. Unnormalizing the output features to their original scale. Args: - config: The configuration object for the SAC policy. + config: The configuration object for the tanh-Gaussian policy. dataset_stats: A dictionary of statistics for normalization. Returns: diff --git a/src/lerobot/processor/hil_processor.py b/src/lerobot/processor/hil_processor.py index 49dbb8106..e7351827b 100644 --- a/src/lerobot/processor/hil_processor.py +++ b/src/lerobot/processor/hil_processor.py @@ -4,7 +4,6 @@ # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# You may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -321,6 +320,7 @@ class GymHILAdapterProcessorStep(ProcessorStep): This step normalizes the `transition` object by: 1. Copying `teleop_action` from `info` to `complementary_data`. 2. Copying `is_intervention` from `info` (using the string key) to `info` (using the enum key). + 3. Copying `discrete_penalty` from `info` to `complementary_data`. """ def __call__(self, transition: EnvTransition) -> EnvTransition: @@ -330,6 +330,9 @@ class GymHILAdapterProcessorStep(ProcessorStep): if TELEOP_ACTION_KEY in info: complementary_data[TELEOP_ACTION_KEY] = info[TELEOP_ACTION_KEY] + if DISCRETE_PENALTY_KEY in info: + complementary_data[DISCRETE_PENALTY_KEY] = info[DISCRETE_PENALTY_KEY] + if "is_intervention" in info: info[TeleopEvents.IS_INTERVENTION] = info["is_intervention"] @@ -348,18 +351,24 @@ class GymHILAdapterProcessorStep(ProcessorStep): @ProcessorStepRegistry.register("gripper_penalty_processor") class GripperPenaltyProcessorStep(ProcessorStep): """ - Applies a penalty for inefficient gripper usage. + Applies a small per-transition cost on the discrete gripper action. - This step penalizes actions that attempt to close an already closed gripper or - open an already open one, based on position thresholds. + Fires only when the commanded action would actually transition the gripper + from one extreme to the other (close-while-open or open-while-closed). + This discourages gripper oscillation while leaving "stay" and saturating-further + commands unpenalized. Attributes: penalty: The negative reward value to apply. max_gripper_pos: The maximum position value for the gripper, used for normalization. + open_threshold: Normalized state below which the gripper is considered "open". + closed_threshold: Normalized state above which the gripper is considered "closed". """ - penalty: float = -0.01 + penalty: float = -0.02 max_gripper_pos: float = 30.0 + open_threshold: float = 0.1 + closed_threshold: float = 0.9 def __call__(self, transition: EnvTransition) -> EnvTransition: """ @@ -379,11 +388,15 @@ class GripperPenaltyProcessorStep(ProcessorStep): if raw_joint_positions is None: return new_transition - current_gripper_pos = raw_joint_positions.get(GRIPPER_KEY, None) + current_gripper_pos = raw_joint_positions.get(f"{GRIPPER_KEY}.pos", None) if current_gripper_pos is None: return new_transition - # Gripper action is a PolicyAction at this stage + # During reset, the transition may not carry any action yet. + if action is None: + return new_transition + + # Gripper action is expected as the last action dimension. gripper_action = action[-1].item() gripper_action_normalized = gripper_action / self.max_gripper_pos @@ -391,9 +404,13 @@ class GripperPenaltyProcessorStep(ProcessorStep): gripper_state_normalized = current_gripper_pos / self.max_gripper_pos # Calculate penalty boolean as in original - gripper_penalty_bool = (gripper_state_normalized < 0.5 and gripper_action_normalized > 0.5) or ( - gripper_state_normalized > 0.75 and gripper_action_normalized < 0.5 - ) + # - currently open AND target is closed -> close transition + # - currently closed AND target is open -> open transition + is_open = gripper_state_normalized < self.open_threshold + is_closed = gripper_state_normalized > self.closed_threshold + cmd_close = gripper_action_normalized > self.closed_threshold + cmd_open = gripper_action_normalized < self.open_threshold + gripper_penalty_bool = (is_open and cmd_close) or (is_closed and cmd_open) gripper_penalty = self.penalty * int(gripper_penalty_bool) @@ -409,11 +426,14 @@ class GripperPenaltyProcessorStep(ProcessorStep): Returns the configuration of the step for serialization. Returns: - A dictionary containing the penalty value and max gripper position. + A dictionary containing the penalty value, max gripper position, + and the open/closed thresholds. """ return { "penalty": self.penalty, "max_gripper_pos": self.max_gripper_pos, + "open_threshold": self.open_threshold, + "closed_threshold": self.closed_threshold, } def reset(self) -> None: diff --git a/src/lerobot/processor/normalize_processor.py b/src/lerobot/processor/normalize_processor.py index 7516c7b47..1649b4b31 100644 --- a/src/lerobot/processor/normalize_processor.py +++ b/src/lerobot/processor/normalize_processor.py @@ -134,6 +134,24 @@ class _NormalizationMixin: if self.dtype is None: self.dtype = torch.float32 self._tensor_stats = to_tensor(self.stats, device=self.device, dtype=self.dtype) + self._reshape_visual_stats() + + def _reshape_visual_stats(self) -> None: + """Reshape flat ``(C,)`` visual stats to ``(C, 1, 1)`` for image broadcasting. + + No-op for stats from :func:`~lerobot.datasets.compute_stats.compute_stats` + (already ``(C, 1, 1)``). Needed by RL training, which can start without + a dataset and supplies stats manually via JSON config. + """ + for key, feature in self.features.items(): + if feature.type != FeatureType.VISUAL: + continue + if key not in self._tensor_stats: + continue + for stat_name, stat_tensor in self._tensor_stats[key].items(): + if not isinstance(stat_tensor, Tensor) or stat_tensor.ndim != 1: + continue + self._tensor_stats[key][stat_name] = stat_tensor.reshape(-1, 1, 1) def to( self, device: torch.device | str | None = None, dtype: torch.dtype | None = None @@ -152,6 +170,7 @@ class _NormalizationMixin: if dtype is not None: self.dtype = dtype self._tensor_stats = to_tensor(self.stats, device=self.device, dtype=self.dtype) + self._reshape_visual_stats() return self def state_dict(self) -> dict[str, Tensor]: @@ -201,6 +220,7 @@ class _NormalizationMixin: # Don't load from state_dict, keep the explicitly provided stats # But ensure _tensor_stats is properly initialized self._tensor_stats = to_tensor(self.stats, device=self.device, dtype=self.dtype) # type: ignore[assignment] + self._reshape_visual_stats() return # Normal behavior: load stats from state_dict @@ -211,6 +231,7 @@ class _NormalizationMixin: self._tensor_stats.setdefault(key, {})[stat_name] = tensor.to( dtype=torch.float32, device=self.device ) + self._reshape_visual_stats() # Reconstruct the original stats dict from tensor stats for compatibility with to() method # and other functions that rely on self.stats diff --git a/src/lerobot/rewards/classifier/configuration_classifier.py b/src/lerobot/rewards/classifier/configuration_classifier.py index a618a2cf7..f1ccaacc6 100644 --- a/src/lerobot/rewards/classifier/configuration_classifier.py +++ b/src/lerobot/rewards/classifier/configuration_classifier.py @@ -30,7 +30,7 @@ class RewardClassifierConfig(RewardModelConfig): latent_dim: int = 256 image_embedding_pooling_dim: int = 8 dropout_rate: float = 0.1 - model_name: str = "helper2424/resnet10" # TODO: This needs to be updated. The model on the Hub doesn't call self.post_init() in its __init__, which is required by transformers v5 to set all_tied_weights_keys. The from_pretrained call fails when it tries to access this attribute during _finalize_model_loading. + model_name: str = "lerobot/resnet10" device: str = "cpu" model_type: str = "cnn" # "transformer" or "cnn" num_cameras: int = 2 diff --git a/src/lerobot/rewards/classifier/modeling_classifier.py b/src/lerobot/rewards/classifier/modeling_classifier.py index bedfffbe9..1d8057135 100644 --- a/src/lerobot/rewards/classifier/modeling_classifier.py +++ b/src/lerobot/rewards/classifier/modeling_classifier.py @@ -105,6 +105,7 @@ class Classifier(PreTrainedRewardModel): def __init__( self, config: RewardClassifierConfig, + **kwargs, ): from transformers import AutoModel diff --git a/src/lerobot/rl/__init__.py b/src/lerobot/rl/__init__.py index 6a7c750d3..8b2d18c54 100644 --- a/src/lerobot/rl/__init__.py +++ b/src/lerobot/rl/__init__.py @@ -12,23 +12,33 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Reinforcement learning modules. +"""Reinforcement learning modules. -Requires: ``pip install 'lerobot[hilserl]'`` - -Available modules (import directly):: - - from lerobot.rl.actor import ... - from lerobot.rl.learner import ... - from lerobot.rl.learner_service import ... - from lerobot.rl.buffer import ... - from lerobot.rl.eval_policy import ... - from lerobot.rl.gym_manipulator import ... +Distributed actor / learner entry points (``actor``, ``learner``, +``learner_service``) require ``pip install 'lerobot[hilserl]'``. Algorithms, +buffer, data sources and trainer are gRPC-free and usable standalone. """ -from lerobot.utils.import_utils import require_package +from .algorithms.base import RLAlgorithm as RLAlgorithm +from .algorithms.configs import RLAlgorithmConfig as RLAlgorithmConfig, TrainingStats as TrainingStats +from .algorithms.factory import ( + make_algorithm as make_algorithm, + make_algorithm_config as make_algorithm_config, +) +from .algorithms.sac.configuration_sac import SACAlgorithmConfig as SACAlgorithmConfig +from .buffer import ReplayBuffer as ReplayBuffer +from .data_sources import DataMixer as DataMixer, OnlineOfflineMixer as OnlineOfflineMixer +from .trainer import RLTrainer as RLTrainer -require_package("grpcio", extra="hilserl", import_name="grpc") - -__all__: list[str] = [] +__all__ = [ + "RLAlgorithm", + "RLAlgorithmConfig", + "TrainingStats", + "make_algorithm", + "make_algorithm_config", + "SACAlgorithmConfig", + "RLTrainer", + "ReplayBuffer", + "DataMixer", + "OnlineOfflineMixer", +] diff --git a/src/lerobot/rl/actor.py b/src/lerobot/rl/actor.py index eab527250..bfc7f1882 100644 --- a/src/lerobot/rl/actor.py +++ b/src/lerobot/rl/actor.py @@ -49,39 +49,53 @@ https://github.com/michel-aractingi/lerobot-hilserl-guide import logging import os import time +from collections.abc import Generator from functools import lru_cache from queue import Empty +from typing import TYPE_CHECKING, Any + +from lerobot.utils.import_utils import _grpc_available, require_package + +if TYPE_CHECKING or _grpc_available: + import grpc + + from lerobot.transport import services_pb2, services_pb2_grpc + from lerobot.transport.utils import ( + bytes_to_state_dict, + grpc_channel_options, + python_object_to_bytes, + receive_bytes_in_chunks, + send_bytes_in_chunks, + transitions_to_bytes, + ) +else: + grpc = None + services_pb2 = None + services_pb2_grpc = None + bytes_to_state_dict = None + grpc_channel_options = None + python_object_to_bytes = None + receive_bytes_in_chunks = None + send_bytes_in_chunks = None + transitions_to_bytes = None -import grpc import torch from torch import nn -from torch.multiprocessing import Event, Queue +from torch.multiprocessing import Queue from lerobot.cameras import opencv # noqa: F401 from lerobot.configs import parser -from lerobot.configs.train import TrainRLServerPipelineConfig -from lerobot.policies import make_policy -from lerobot.policies.sac.modeling_sac import SACPolicy +from lerobot.policies import make_policy, make_pre_post_processors +from lerobot.processor import TransitionKey from lerobot.robots import so_follower # noqa: F401 from lerobot.teleoperators import gamepad, so_leader # noqa: F401 from lerobot.teleoperators.utils import TeleopEvents -from lerobot.transport import services_pb2, services_pb2_grpc -from lerobot.transport.utils import ( - bytes_to_state_dict, - grpc_channel_options, - python_object_to_bytes, - receive_bytes_in_chunks, - send_bytes_in_chunks, - transitions_to_bytes, -) -from lerobot.types import TransitionKey from lerobot.utils.device_utils import get_safe_torch_device from lerobot.utils.process import ProcessSignalHandler from lerobot.utils.random_utils import set_seed from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.transition import ( Transition, - move_state_dict_to_device, move_transition_to_device, ) from lerobot.utils.utils import ( @@ -89,19 +103,24 @@ from lerobot.utils.utils import ( init_logging, ) +from .algorithms.base import RLAlgorithm +from .algorithms.factory import make_algorithm from .gym_manipulator import ( - create_transition, make_processors, make_robot_env, + reset_and_build_transition, step_env_and_process_transition, ) from .queue import get_last_item_from_queue +from .train_rl import TrainRLServerPipelineConfig # Main entry point @parser.wrap() def actor_cli(cfg: TrainRLServerPipelineConfig): + # Fail fast with a friendly error if the optional ``hilserl`` extra is missing. + require_package("grpcio", extra="hilserl", import_name="grpc") cfg.validate() display_pid = False if not use_threads(cfg): @@ -212,7 +231,7 @@ def actor_cli(cfg: TrainRLServerPipelineConfig): def act_with_policy( cfg: TrainRLServerPipelineConfig, - shutdown_event: any, # Event, + shutdown_event: Any, # Event parameters_queue: Queue, transitions_queue: Queue, interactions_queue: Queue, @@ -252,22 +271,24 @@ def act_with_policy( logging.info("make_policy") ### Instantiate the policy in both the actor and learner processes - ### To avoid sending a SACPolicy object through the port, we create a policy instance + ### To avoid sending a policy object through the port, we create a policy instance ### on both sides, the learner sends the updated parameters every n steps to update the actor's parameters - policy: SACPolicy = make_policy( + policy = make_policy( cfg=cfg.policy, env_cfg=cfg.env, ) - policy = policy.eval() + policy = policy.to(device).eval() assert isinstance(policy, nn.Module) - obs, info = online_env.reset() - env_processor.reset() - action_processor.reset() + # Build the algorithm + algorithm = make_algorithm(cfg=cfg.algorithm, policy=policy) - # Process initial observation - transition = create_transition(observation=obs, info=info) - transition = env_processor(transition) + preprocessor, postprocessor = make_pre_post_processors( + policy_cfg=cfg.policy, + dataset_stats=cfg.policy.dataset_stats, + ) + + transition = reset_and_build_transition(online_env, env_processor, action_processor) # NOTE: For the moment we will solely handle the case of a single environment sum_reward_episode = 0 @@ -291,8 +312,17 @@ def act_with_policy( # Time policy inference and check if it meets FPS requirement with policy_timer: - # Extract observation from transition for policy - action = policy.select_action(batch=observation) + normalized_observation = preprocessor.process_observation(observation) + action = policy.select_action(batch=normalized_observation) + # Unnormalize only the continuous part. + if cfg.policy.num_discrete_actions is not None: + continuous_action = postprocessor.process_action(action[..., :-1]) + discrete_action = action[..., -1:].to( + device=continuous_action.device, dtype=continuous_action.dtype + ) + action = torch.cat([continuous_action, discrete_action], dim=-1) + else: + action = postprocessor.process_action(action) policy_fps = policy_timer.fps_last log_policy_frequency_issue(policy_fps=policy_fps, cfg=cfg, interaction_step=interaction_step) @@ -326,7 +356,8 @@ def act_with_policy( # Check for intervention from transition info intervention_info = new_transition[TransitionKey.INFO] - if intervention_info.get(TeleopEvents.IS_INTERVENTION, False): + is_intervention = bool(intervention_info.get(TeleopEvents.IS_INTERVENTION, False)) + if is_intervention: episode_intervention = True episode_intervention_steps += 1 @@ -334,6 +365,7 @@ def act_with_policy( "discrete_penalty": torch.tensor( [new_transition[TransitionKey.COMPLEMENTARY_DATA].get("discrete_penalty", 0.0)] ), + TeleopEvents.IS_INTERVENTION.value: is_intervention, } # Create transition for learner (convert to old format) list_transition_to_send_to_learner.append( @@ -354,7 +386,7 @@ def act_with_policy( if done or truncated: logging.info(f"[ACTOR] Global step {interaction_step}: Episode reward: {sum_reward_episode}") - update_policy_parameters(policy=policy, parameters_queue=parameters_queue, device=device) + update_policy_parameters(algorithm=algorithm, parameters_queue=parameters_queue, device=device) if len(list_transition_to_send_to_learner) > 0: push_transitions_to_transport_queue( @@ -390,14 +422,7 @@ def act_with_policy( episode_intervention_steps = 0 episode_total_steps = 0 - # Reset environment and processors - obs, info = online_env.reset() - env_processor.reset() - action_processor.reset() - - # Process initial observation - transition = create_transition(observation=obs, info=info) - transition = env_processor(transition) + transition = reset_and_build_transition(online_env, env_processor, action_processor) if cfg.env.fps is not None: dt_time = time.perf_counter() - start_time @@ -408,10 +433,10 @@ def act_with_policy( def establish_learner_connection( - stub: services_pb2_grpc.LearnerServiceStub, - shutdown_event: Event, # type: ignore + stub: "services_pb2_grpc.LearnerServiceStub", + shutdown_event: Any, # Event attempts: int = 30, -): +) -> bool: """Establish a connection with the learner. Args: @@ -441,12 +466,14 @@ def establish_learner_connection( def learner_service_client( host: str = "127.0.0.1", port: int = 50051, -) -> tuple[services_pb2_grpc.LearnerServiceStub, grpc.Channel]: - """ - Returns a client for the learner service. +) -> "tuple[services_pb2_grpc.LearnerServiceStub, grpc.Channel]": + """Return a client for the learner service. GRPC uses HTTP/2, which is a binary protocol and multiplexes requests over a single connection. So we need to create only one client and reuse it. + + Returns: + tuple[services_pb2_grpc.LearnerServiceStub, grpc.Channel]: The stub and the channel. """ channel = grpc.insecure_channel( @@ -461,16 +488,18 @@ def learner_service_client( def receive_policy( cfg: TrainRLServerPipelineConfig, parameters_queue: Queue, - shutdown_event: Event, # type: ignore - learner_client: services_pb2_grpc.LearnerServiceStub | None = None, - grpc_channel: grpc.Channel | None = None, -): + shutdown_event: Any, # Event + learner_client: "services_pb2_grpc.LearnerServiceStub | None" = None, + grpc_channel: "grpc.Channel | None" = None, +) -> None: """Receive parameters from the learner. Args: cfg (TrainRLServerPipelineConfig): The configuration for the actor. parameters_queue (Queue): The queue to receive the parameters. shutdown_event (Event): The event to check if the process should shutdown. + learner_client (services_pb2_grpc.LearnerServiceStub | None): Optional pre-created stub. + grpc_channel (grpc.Channel | None): Optional pre-created channel. """ logging.info("[ACTOR] Start receiving parameters from the Learner") if not use_threads(cfg): @@ -513,12 +542,11 @@ def receive_policy( def send_transitions( cfg: TrainRLServerPipelineConfig, transitions_queue: Queue, - shutdown_event: any, # Event, - learner_client: services_pb2_grpc.LearnerServiceStub | None = None, - grpc_channel: grpc.Channel | None = None, -) -> services_pb2.Empty: - """ - Sends transitions to the learner. + shutdown_event: Any, # Event + learner_client: "services_pb2_grpc.LearnerServiceStub | None" = None, + grpc_channel: "grpc.Channel | None" = None, +) -> None: + """Send transitions to the learner. This function continuously retrieves messages from the queue and processes: @@ -526,6 +554,13 @@ def send_transitions( - A batch of transitions (observation, action, reward, next observation) is collected. - Transitions are moved to the CPU and serialized using PyTorch. - The serialized data is wrapped in a `services_pb2.Transition` message and sent to the learner. + + Args: + cfg (TrainRLServerPipelineConfig): The configuration for the actor. + transitions_queue (Queue): The queue to receive the transitions. + shutdown_event (Event): The event to check if the process should shutdown. + learner_client (services_pb2_grpc.LearnerServiceStub | None): Optional pre-created stub. + grpc_channel (grpc.Channel | None): Optional pre-created channel. """ if not use_threads(cfg): @@ -563,18 +598,24 @@ def send_transitions( def send_interactions( cfg: TrainRLServerPipelineConfig, interactions_queue: Queue, - shutdown_event: Event, # type: ignore - learner_client: services_pb2_grpc.LearnerServiceStub | None = None, - grpc_channel: grpc.Channel | None = None, -) -> services_pb2.Empty: - """ - Sends interactions to the learner. + shutdown_event: Any, # Event + learner_client: "services_pb2_grpc.LearnerServiceStub | None" = None, + grpc_channel: "grpc.Channel | None" = None, +) -> None: + """Send interactions to the learner. This function continuously retrieves messages from the queue and processes: - Interaction Messages: - Contains useful statistics about episodic rewards and policy timings. - The message is serialized using `pickle` and sent to the learner. + + Args: + cfg (TrainRLServerPipelineConfig): The configuration for the actor. + interactions_queue (Queue): The queue to receive the interactions. + shutdown_event (Event): The event to check if the process should shutdown. + learner_client (services_pb2_grpc.LearnerServiceStub | None): Optional pre-created stub. + grpc_channel (grpc.Channel | None): Optional pre-created channel. """ if not use_threads(cfg): @@ -613,7 +654,11 @@ def send_interactions( logging.info("[ACTOR] Interactions process stopped") -def transitions_stream(shutdown_event: Event, transitions_queue: Queue, timeout: float) -> services_pb2.Empty: # type: ignore +def transitions_stream( + shutdown_event: Any, # Event + transitions_queue: Queue, + timeout: float, +) -> "Generator[Any, None, services_pb2.Empty]": while not shutdown_event.is_set(): try: message = transitions_queue.get(block=True, timeout=timeout) @@ -629,10 +674,10 @@ def transitions_stream(shutdown_event: Event, transitions_queue: Queue, timeout: def interactions_stream( - shutdown_event: Event, + shutdown_event: Any, # Event interactions_queue: Queue, - timeout: float, # type: ignore -) -> services_pb2.Empty: + timeout: float, +) -> "Generator[Any, None, services_pb2.Empty]": while not shutdown_event.is_set(): try: message = interactions_queue.get(block=True, timeout=timeout) @@ -652,7 +697,8 @@ def interactions_stream( # Policy functions -def update_policy_parameters(policy: SACPolicy, parameters_queue: Queue, device): +def update_policy_parameters(algorithm: RLAlgorithm, parameters_queue: Queue, device): + """Drain the latest learner-pushed weights into ``algorithm.policy``.""" bytes_state_dict = get_last_item_from_queue(parameters_queue, block=False) if bytes_state_dict is not None: logging.info("[ACTOR] Load new parameters from Learner.") @@ -667,18 +713,7 @@ def update_policy_parameters(policy: SACPolicy, parameters_queue: Queue, device) # - Send critic's encoder state when shared_encoder=True # - Skip encoder params entirely when freeze_vision_encoder=True # - Ensure discrete_critic gets correct encoder state (currently uses encoder_critic) - - # Load actor state dict - actor_state_dict = move_state_dict_to_device(state_dicts["policy"], device=device) - policy.actor.load_state_dict(actor_state_dict) - - # Load discrete critic if present - if hasattr(policy, "discrete_critic") and "discrete_critic" in state_dicts: - discrete_critic_state_dict = move_state_dict_to_device( - state_dicts["discrete_critic"], device=device - ) - policy.discrete_critic.load_state_dict(discrete_critic_state_dict) - logging.info("[ACTOR] Loaded discrete critic parameters from Learner.") + algorithm.load_weights(state_dicts, device=device) # Utilities functions diff --git a/src/lerobot/rl/algorithms/__init__.py b/src/lerobot/rl/algorithms/__init__.py new file mode 100644 index 000000000..c09bd26fc --- /dev/null +++ b/src/lerobot/rl/algorithms/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .sac import SACAlgorithm, SACAlgorithmConfig + +__all__ = [ + "SACAlgorithm", + "SACAlgorithmConfig", +] diff --git a/src/lerobot/rl/algorithms/base.py b/src/lerobot/rl/algorithms/base.py new file mode 100644 index 000000000..01c34584b --- /dev/null +++ b/src/lerobot/rl/algorithms/base.py @@ -0,0 +1,207 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import abc +import builtins +import os +from collections.abc import Iterator +from pathlib import Path +from typing import TYPE_CHECKING, Any, TypeVar + +import torch +from huggingface_hub import hf_hub_download +from huggingface_hub.constants import SAFETENSORS_SINGLE_FILE +from huggingface_hub.errors import HfHubHTTPError +from safetensors.torch import load_file as load_safetensors, save_file as save_safetensors +from torch.optim import Optimizer + +from lerobot.types import BatchType +from lerobot.utils.hub import HubMixin + +from .configs import RLAlgorithmConfig, TrainingStats + +if TYPE_CHECKING: + from torch import nn + + from ..data_sources.data_mixer import DataMixer + +T = TypeVar("T", bound="RLAlgorithm") + + +class RLAlgorithm(HubMixin, abc.ABC): + """Base for all RL algorithms.""" + + config_class: type[RLAlgorithmConfig] + name: str + config: RLAlgorithmConfig + + @abc.abstractmethod + def update(self, batch_iterator: Iterator[BatchType]) -> TrainingStats: + """One complete training step. + + The algorithm calls ``next(batch_iterator)`` as many times as it + needs (e.g. ``utd_ratio`` times for SAC) to obtain fresh batches. + The iterator is owned by the trainer; the algorithm just consumes + from it. + """ + raise NotImplementedError + + def configure_data_iterator( + self, + data_mixer: DataMixer, + batch_size: int, + *, + async_prefetch: bool = True, + queue_size: int = 2, + ) -> Iterator[BatchType]: + """Create the data iterator this algorithm needs. + + The default implementation uses the standard ``data_mixer.get_iterator()``. + Algorithms that need specialised sampling should override this method. + """ + return data_mixer.get_iterator( + batch_size=batch_size, + async_prefetch=async_prefetch, + queue_size=queue_size, + ) + + @abc.abstractmethod + def make_optimizers_and_scheduler(self) -> dict[str, Optimizer]: + """Build and return the optimizers used during training. + + Called once on the learner side after construction. + """ + raise NotImplementedError + + def get_optimizers(self) -> dict[str, Optimizer]: + """Return optimizers for checkpointing / external scheduling.""" + return {} + + @property + def optimization_step(self) -> int: + """Current learner optimization step. + + Part of the stable contract for checkpoint/resume. Algorithms can + either use this default storage or override for custom behavior. + """ + return getattr(self, "_optimization_step", 0) + + @optimization_step.setter + def optimization_step(self, value: int) -> None: + self._optimization_step = int(value) + + def get_weights(self) -> dict[str, Any]: + """Policy state-dict to push to actors.""" + return {} + + @abc.abstractmethod + def load_weights(self, weights: dict[str, Any], device: str | torch.device = "cpu") -> None: + """Load policy state-dict received from the learner.""" + raise NotImplementedError + + @abc.abstractmethod + def state_dict(self) -> dict[str, torch.Tensor]: + """Algorithm-owned trainable tensors. + + Must return a flat tensor mapping for everything the algorithm owns + that is not part of the policy (e.g. critic ensembles, target networks, + temperature parameters). Algorithms with no training-only tensors + should explicitly return an empty dict. + """ + raise NotImplementedError + + @abc.abstractmethod + def load_state_dict( + self, + state_dict: dict[str, torch.Tensor], + device: str | torch.device = "cpu", + ) -> None: + """In-place load of algorithm-owned tensors. + + Implementations MUST keep the identity of any ``nn.Parameter`` that an + optimizer references (e.g. SAC's ``log_alpha``) by using ``.copy_()`` + rather than rebinding the attribute. + """ + raise NotImplementedError + + def _save_pretrained(self, save_directory: Path) -> None: + """Persist the algorithm's tensors and config to ``save_directory``. + + Writes ``model.safetensors`` (algorithm tensors via :meth:`state_dict`) + and ``config.json`` (via :meth:`RLAlgorithmConfig.save_pretrained`). + """ + tensors = {k: v.detach().cpu().contiguous() for k, v in self.state_dict().items()} + save_safetensors(tensors, str(save_directory / SAFETENSORS_SINGLE_FILE)) + self.config._save_pretrained(save_directory) + + @classmethod + def from_pretrained( + cls: builtins.type[T], + pretrained_name_or_path: str | Path, + *, + policy: nn.Module, + config: RLAlgorithmConfig | None = None, + force_download: bool = False, + resume_download: bool | None = None, + proxies: dict | None = None, + token: str | bool | None = None, + cache_dir: str | Path | None = None, + local_files_only: bool = False, + revision: str | None = None, + device: str | torch.device = "cpu", + **algo_kwargs: Any, + ) -> T: + """Build an algorithm and load its weights from ``pretrained_name_or_path``.""" + if config is None: + config = cls.config_class.from_pretrained( + pretrained_name_or_path, + force_download=force_download, + resume_download=resume_download, + proxies=proxies, + token=token, + cache_dir=cache_dir, + local_files_only=local_files_only, + revision=revision, + ) + if hasattr(config, "policy_config"): + config.policy_config = policy.config + + instance = cls(policy=policy, config=config, **algo_kwargs) + + model_id = str(pretrained_name_or_path) + if os.path.isdir(model_id): + model_file = os.path.join(model_id, SAFETENSORS_SINGLE_FILE) + else: + try: + model_file = hf_hub_download( + repo_id=model_id, + filename=SAFETENSORS_SINGLE_FILE, + revision=revision, + cache_dir=cache_dir, + force_download=force_download, + proxies=proxies, + resume_download=resume_download, + token=token, + local_files_only=local_files_only, + ) + except HfHubHTTPError as e: + raise FileNotFoundError( + f"{SAFETENSORS_SINGLE_FILE} not found on the HuggingFace Hub in {model_id}" + ) from e + + tensors = load_safetensors(model_file) + instance.load_state_dict(tensors, device=device) + return instance diff --git a/src/lerobot/rl/algorithms/configs.py b/src/lerobot/rl/algorithms/configs.py new file mode 100644 index 000000000..9448afeb3 --- /dev/null +++ b/src/lerobot/rl/algorithms/configs.py @@ -0,0 +1,138 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import abc +import builtins +import logging +import os +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, TypeVar + +import draccus +from huggingface_hub import hf_hub_download +from huggingface_hub.constants import CONFIG_NAME +from huggingface_hub.errors import HfHubHTTPError + +from lerobot.utils.hub import HubMixin + +T = TypeVar("T", bound="RLAlgorithmConfig") + +logger = logging.getLogger(__name__) + + +@dataclass +class TrainingStats: + """Returned by ``algorithm.update()`` for logging and checkpointing.""" + + losses: dict[str, float] = field(default_factory=dict) + grad_norms: dict[str, float] = field(default_factory=dict) + extra: dict[str, float] = field(default_factory=dict) + + def to_log_dict(self) -> dict[str, float]: + """Flatten all stats into a single dict for logging.""" + + d: dict[str, float] = {} + for name, val in self.losses.items(): + d[name] = val + for name, val in self.grad_norms.items(): + d[f"{name}_grad_norm"] = val + for name, val in self.extra.items(): + d[name] = val + return d + + +@dataclass +class RLAlgorithmConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC): + """Registry for algorithm configs.""" + + @property + def type(self) -> str: + """Registered name of this algorithm config (e.g. ``"sac"``).""" + choice_name = self.get_choice_name(self.__class__) + if not isinstance(choice_name, str): + raise TypeError(f"Expected string from get_choice_name, got {type(choice_name)}") + return choice_name + + @classmethod + @abc.abstractmethod + def from_policy_config(cls, policy_cfg: Any) -> RLAlgorithmConfig: + """Build an algorithm config from a policy config. + + Must be overridden by every registered config subclass. + """ + raise NotImplementedError(f"{cls.__name__} must implement from_policy_config()") + + def _save_pretrained(self, save_directory: Path) -> None: + """Serialize this config as ``config.json`` inside ``save_directory``.""" + with open(save_directory / CONFIG_NAME, "w") as f, draccus.config_type("json"): + draccus.dump(self, f, indent=4) + + @classmethod + def from_pretrained( + cls: builtins.type[T], + pretrained_name_or_path: str | Path, + *, + force_download: bool = False, + resume_download: bool | None = None, + proxies: dict[Any, Any] | None = None, + token: str | bool | None = None, + cache_dir: str | Path | None = None, + local_files_only: bool = False, + revision: str | None = None, + **algo_kwargs: Any, + ) -> T: + model_id = str(pretrained_name_or_path) + config_file: str | None = None + if Path(model_id).is_dir(): + if CONFIG_NAME in os.listdir(model_id): + config_file = os.path.join(model_id, CONFIG_NAME) + else: + logger.error(f"{CONFIG_NAME} not found in {Path(model_id).resolve()}") + else: + try: + config_file = hf_hub_download( + repo_id=model_id, + filename=CONFIG_NAME, + revision=revision, + cache_dir=cache_dir, + force_download=force_download, + proxies=proxies, + resume_download=resume_download, + token=token, + local_files_only=local_files_only, + ) + except HfHubHTTPError as e: + raise FileNotFoundError( + f"{CONFIG_NAME} not found on the HuggingFace Hub in {model_id}" + ) from e + + if config_file is None: + raise FileNotFoundError(f"{CONFIG_NAME} not found in {model_id}") + + with draccus.config_type("json"): + instance = draccus.parse(RLAlgorithmConfig, config_file, args=[]) + + if cls is not RLAlgorithmConfig and not isinstance(instance, cls): + raise TypeError( + f"Config at {model_id} has type '{instance.type}' but was loaded via " + f"{cls.__name__}; use the matching subclass or RLAlgorithmConfig.from_pretrained()." + ) + + for key, value in algo_kwargs.items(): + if hasattr(instance, key): + setattr(instance, key, value) + return instance diff --git a/src/lerobot/rl/algorithms/factory.py b/src/lerobot/rl/algorithms/factory.py new file mode 100644 index 000000000..2a5d9dea7 --- /dev/null +++ b/src/lerobot/rl/algorithms/factory.py @@ -0,0 +1,99 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import torch + +from .base import RLAlgorithm +from .configs import RLAlgorithmConfig + + +def make_algorithm_config(algorithm_type: str, **kwargs) -> RLAlgorithmConfig: + """Instantiate an `RLAlgorithmConfig` from its registered type name. + + Args: + algorithm_type: Registry key of the algorithm (e.g. ``"sac"``). + **kwargs: Keyword arguments forwarded to the config class constructor. + + Returns: + An instance of the matching ``RLAlgorithmConfig`` subclass. + + Raises: + ValueError: If ``algorithm_type`` is not registered. + """ + try: + cls = RLAlgorithmConfig.get_choice_class(algorithm_type) + except KeyError as err: + raise ValueError( + f"Algorithm type '{algorithm_type}' is not registered. " + f"Available: {list(RLAlgorithmConfig.get_known_choices().keys())}" + ) from err + return cls(**kwargs) + + +def get_algorithm_class(name: str) -> type[RLAlgorithm]: + """ + Retrieves an RL algorithm class by its registered name. + + This function uses dynamic imports to avoid loading all algorithm classes into + memory at once, improving startup time and reducing dependencies. + + Args: + name: The name of the algorithm. Supported names are "sac". + + Returns: + The algorithm class corresponding to the given name. + + Raises: + ValueError: If the algorithm name is not recognized. + """ + if name == "sac": + from .sac.sac_algorithm import SACAlgorithm + + return SACAlgorithm + raise ValueError( + f"Algorithm type '{name}' is not available. " + f"Known: {list(RLAlgorithmConfig.get_known_choices().keys())}" + ) + + +def make_algorithm(cfg: RLAlgorithmConfig, policy: torch.nn.Module) -> RLAlgorithm: + """ + Instantiate an RL algorithm. + + This factory function looks up the :class:`RLAlgorithm` subclass that matches + ``cfg.type`` and instantiates it with the provided policy. It also enforces + that ``cfg.policy_config`` has been populated before construction (this is + normally handled by :meth:`TrainRLServerPipelineConfig.validate`). + + Args: + cfg: The algorithm configuration. Must have ``policy_config`` set. + policy: The policy module the algorithm will train. + + Returns: + An instantiated :class:`RLAlgorithm`. + + Raises: + ValueError: If ``cfg.policy_config`` is ``None`` or ``cfg.type`` is not + registered. + """ + if getattr(cfg, "policy_config", None) is None: + raise ValueError( + f"{type(cfg).__name__}.policy_config is None. " + "It must be populated (typically by TrainRLServerPipelineConfig.validate) " + "before calling make_algorithm()." + ) + cls = get_algorithm_class(cfg.type) + return cls(policy=policy, config=cfg) diff --git a/src/lerobot/rl/algorithms/sac/__init__.py b/src/lerobot/rl/algorithms/sac/__init__.py new file mode 100644 index 000000000..9d076bcbb --- /dev/null +++ b/src/lerobot/rl/algorithms/sac/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .configuration_sac import SACAlgorithmConfig +from .sac_algorithm import SACAlgorithm + +__all__ = ["SACAlgorithm", "SACAlgorithmConfig"] diff --git a/src/lerobot/rl/algorithms/sac/configuration_sac.py b/src/lerobot/rl/algorithms/sac/configuration_sac.py new file mode 100644 index 000000000..c4e9b334a --- /dev/null +++ b/src/lerobot/rl/algorithms/sac/configuration_sac.py @@ -0,0 +1,99 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass, field + +from lerobot.configs.policies import PreTrainedConfig +from lerobot.policies.gaussian_actor.configuration_gaussian_actor import ( + CriticNetworkConfig, + GaussianActorConfig, +) + +from ..configs import RLAlgorithmConfig + + +@RLAlgorithmConfig.register_subclass("sac") +@dataclass +class SACAlgorithmConfig(RLAlgorithmConfig): + """Soft Actor-Critic (SAC) algorithm configuration. + + SAC is an off-policy actor-critic deep RL algorithm based on the maximum + entropy reinforcement learning framework. It learns a policy and a Q-function + simultaneously using experience collected from the environment. + + This configuration class contains the algorithm-side hyperparameters: critic + ensemble, target networks, temperature / entropy tuning, and the Bellman + update loop. The policy-side (actor + observation encoder) lives in + :class:`~lerobot.policies.gaussian_actor.GaussianActorConfig` and is + referenced via :attr:`policy_config`. + """ + + # Optimizer learning rates + # Learning rate for the actor network + actor_lr: float = 3e-4 + # Learning rate for the critic network + critic_lr: float = 3e-4 + # Learning rate for the temperature parameter + temperature_lr: float = 3e-4 + + # Bellman update + # Discount factor for the SAC algorithm + discount: float = 0.99 + # Whether to use backup entropy for the SAC algorithm + use_backup_entropy: bool = True + # Weight for the critic target update + critic_target_update_weight: float = 0.005 + + # Critic ensemble + # Number of critics in the ensemble + num_critics: int = 2 + # Number of subsampled critics for training + num_subsample_critics: int | None = None + # Configuration for the critic network architecture + critic_network_kwargs: CriticNetworkConfig = field(default_factory=CriticNetworkConfig) + # Configuration for the discrete critic network + discrete_critic_network_kwargs: CriticNetworkConfig = field(default_factory=CriticNetworkConfig) + + # Temperature / entropy + # Initial temperature value + temperature_init: float = 1.0 + # Target entropy for automatic temperature tuning. If ``None``, defaults to + # ``-|A|/2`` where ``|A|`` is the total action dimension (continuous + 1 if + # there is a discrete action head). + target_entropy: float | None = None + + # Update loop + # Update-to-data ratio. Set to >1 to enable extra critic updates per env step. + utd_ratio: int = 1 + # Frequency of policy updates + policy_update_freq: int = 1 + # Gradient clipping norm for the SAC algorithm + grad_clip_norm: float = 40.0 + + # Optimizations + # torch.compile is currently disabled by default + use_torch_compile: bool = False + + # Policy config + policy_config: PreTrainedConfig | None = None + + @classmethod + def from_policy_config(cls, policy_cfg: GaussianActorConfig) -> SACAlgorithmConfig: + """Build an algorithm config with default hyperparameters for a given policy.""" + return cls( + policy_config=policy_cfg, + discrete_critic_network_kwargs=policy_cfg.discrete_critic_network_kwargs, + ) diff --git a/src/lerobot/rl/algorithms/sac/sac_algorithm.py b/src/lerobot/rl/algorithms/sac/sac_algorithm.py new file mode 100644 index 000000000..81c44068f --- /dev/null +++ b/src/lerobot/rl/algorithms/sac/sac_algorithm.py @@ -0,0 +1,672 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import math +from collections.abc import Callable, Iterator +from dataclasses import asdict +from typing import Any + +import einops +import torch +import torch.nn as nn +import torch.nn.functional as F # noqa: N812 +from torch import Tensor +from torch.optim import Optimizer + +from lerobot.policies.gaussian_actor.modeling_gaussian_actor import ( + DISCRETE_DIMENSION_INDEX, + MLP, + DiscreteCritic, + GaussianActorObservationEncoder, + GaussianActorPolicy, + orthogonal_init, +) +from lerobot.policies.utils import get_device_from_parameters +from lerobot.types import BatchType +from lerobot.utils.constants import ACTION +from lerobot.utils.transition import move_state_dict_to_device + +from ..base import RLAlgorithm +from ..configs import TrainingStats +from .configuration_sac import SACAlgorithmConfig + + +class SACAlgorithm(RLAlgorithm): + """Soft Actor-Critic. Owns critics, targets, temperature, and loss computation.""" + + config_class = SACAlgorithmConfig + name = "sac" + + def __init__( + self, + policy: GaussianActorPolicy, + config: SACAlgorithmConfig, + ): + self.config = config + self.policy_config = config.policy_config + self.policy = policy + self.optimizers: dict[str, Optimizer] = {} + self._optimization_step: int = 0 + + action_dim = self.policy.config.output_features[ACTION].shape[0] + self._init_critics(action_dim) + self._init_temperature(action_dim) + + self._device = torch.device(self.policy.config.device) + self._move_to_device() + + def _init_critics(self, action_dim) -> None: + """Build critic ensemble, targets.""" + encoder = self.policy.encoder_critic + + heads = [ + CriticHead( + input_dim=encoder.output_dim + action_dim, + **asdict(self.config.critic_network_kwargs), + ) + for _ in range(self.config.num_critics) + ] + self.critic_ensemble = CriticEnsemble(encoder=encoder, ensemble=heads) + target_heads = [ + CriticHead( + input_dim=encoder.output_dim + action_dim, + **asdict(self.config.critic_network_kwargs), + ) + for _ in range(self.config.num_critics) + ] + self.critic_target = CriticEnsemble(encoder=encoder, ensemble=target_heads) + self.critic_target.load_state_dict(self.critic_ensemble.state_dict()) + + # TODO(Khalil): Investigate and fix torch.compile + # NOTE: torch.compile is disabled, policy does not converge when enabled. + if self.config.use_torch_compile: + self.critic_ensemble = torch.compile(self.critic_ensemble) + self.critic_target = torch.compile(self.critic_target) + + self.discrete_critic_target = None + if self.policy_config.num_discrete_actions is not None: + self.discrete_critic_target = self._init_discrete_critic_target(encoder) + + def _init_discrete_critic_target(self, encoder: GaussianActorObservationEncoder) -> DiscreteCritic: + """Build target discrete critic (main network is owned by the policy).""" + discrete_critic_target = DiscreteCritic( + encoder=encoder, + input_dim=encoder.output_dim, + output_dim=self.policy_config.num_discrete_actions, + **asdict(self.config.discrete_critic_network_kwargs), + ) + # TODO(Khalil): Compile the discrete critic + discrete_critic_target.load_state_dict(self.policy.discrete_critic.state_dict()) + return discrete_critic_target + + def _init_temperature(self, continuous_action_dim: int) -> None: + """Set up temperature parameter (log_alpha) and target entropy.""" + temp_init = self.config.temperature_init + self.log_alpha = nn.Parameter(torch.tensor([math.log(temp_init)])) + + self.target_entropy = self.config.target_entropy + if self.target_entropy is None: + total_action_dim = continuous_action_dim + ( + 1 if self.policy_config.num_discrete_actions is not None else 0 + ) + self.target_entropy = -total_action_dim / 2 + + def _move_to_device(self) -> None: + self.policy.to(self._device) + self.critic_ensemble.to(self._device) + self.critic_target.to(self._device) + self.log_alpha = nn.Parameter(self.log_alpha.data.to(self._device)) + if self.discrete_critic_target is not None: + self.discrete_critic_target.to(self._device) + + @property + def temperature(self) -> float: + """Return the current temperature value, always in sync with log_alpha.""" + return self.log_alpha.exp().item() + + def _critic_forward( + self, + observations: dict[str, Tensor], + actions: Tensor, + use_target: bool = False, + observation_features: Tensor | None = None, + ) -> Tensor: + """Forward pass through a critic network ensemble + + Args: + observations: Dictionary of observations + actions: Action tensor + use_target: If True, use target critics, otherwise use ensemble critics + + Returns: + Tensor of Q-values from all critics + """ + + critics = self.critic_target if use_target else self.critic_ensemble + q_values = critics(observations, actions, observation_features) + return q_values + + def _discrete_critic_forward( + self, observations, use_target=False, observation_features=None + ) -> torch.Tensor: + """Forward pass through a discrete critic network + + Args: + observations: Dictionary of observations + use_target: If True, use target critics, otherwise use ensemble critics + observation_features: Optional pre-computed observation features to avoid recomputing encoder output + + Returns: + Tensor of Q-values from the discrete critic network + """ + discrete_critic = self.discrete_critic_target if use_target else self.policy.discrete_critic + q_values = discrete_critic(observations, observation_features) + return q_values + + def update(self, batch_iterator: Iterator[BatchType]) -> TrainingStats: + """Run one SAC training step (critic / discrete-critic / actor / temperature). + + Pulls ``utd_ratio`` batches from ``batch_iterator``, computes the relevant + losses, backpropagates each, and updates target networks. + + Args: + batch_iterator: yields batches each containing + - ``action``: Action tensor + - ``reward``: Reward tensor + - ``state``: Observations tensor dict + - ``next_state``: Next observations tensor dict + - ``done``: Done mask tensor + - ``observation_feature``: Optional pre-computed observation features + - ``next_observation_feature``: Optional pre-computed next observation features + - ``complementary_info`` (optional): per-step extras like discrete penalties + + Returns: + TrainingStats with per-component losses and grad norms. + """ + clip = self.config.grad_clip_norm + + for _ in range(self.config.utd_ratio - 1): + batch = next(batch_iterator) + fb = self._prepare_forward_batch(batch, include_complementary_info=True) + + loss_critic = self._compute_loss_critic(fb) + self.optimizers["critic"].zero_grad() + loss_critic.backward() + torch.nn.utils.clip_grad_norm_(self.critic_ensemble.parameters(), max_norm=clip) + self.optimizers["critic"].step() + + if self.policy_config.num_discrete_actions is not None: + loss_dc = self._compute_loss_discrete_critic(fb) + self.optimizers["discrete_critic"].zero_grad() + loss_dc.backward() + torch.nn.utils.clip_grad_norm_(self.policy.discrete_critic.parameters(), max_norm=clip) + self.optimizers["discrete_critic"].step() + + self._update_target_networks() + + batch = next(batch_iterator) + fb = self._prepare_forward_batch(batch, include_complementary_info=False) + + loss_critic = self._compute_loss_critic(fb) + self.optimizers["critic"].zero_grad() + loss_critic.backward() + critic_grad = torch.nn.utils.clip_grad_norm_(self.critic_ensemble.parameters(), max_norm=clip).item() + self.optimizers["critic"].step() + + stats = TrainingStats( + losses={"loss_critic": loss_critic.item()}, + grad_norms={"critic": critic_grad}, + ) + + if self.policy_config.num_discrete_actions is not None: + loss_dc = self._compute_loss_discrete_critic(fb) + self.optimizers["discrete_critic"].zero_grad() + loss_dc.backward() + dc_grad = torch.nn.utils.clip_grad_norm_( + self.policy.discrete_critic.parameters(), max_norm=clip + ).item() + self.optimizers["discrete_critic"].step() + stats.losses["loss_discrete_critic"] = loss_dc.item() + stats.grad_norms["discrete_critic"] = dc_grad + + if self._optimization_step % self.config.policy_update_freq == 0: + for _ in range(self.config.policy_update_freq): + loss_actor = self._compute_loss_actor(fb) + self.optimizers["actor"].zero_grad() + loss_actor.backward() + actor_grad = torch.nn.utils.clip_grad_norm_( + self.policy.actor.parameters(), max_norm=clip + ).item() + self.optimizers["actor"].step() + + loss_temp = self._compute_loss_temperature(fb) + self.optimizers["temperature"].zero_grad() + loss_temp.backward() + temp_grad = torch.nn.utils.clip_grad_norm_([self.log_alpha], max_norm=clip).item() + self.optimizers["temperature"].step() + + stats.losses["loss_actor"] = loss_actor.item() + stats.losses["loss_temperature"] = loss_temp.item() + stats.grad_norms["actor"] = actor_grad + stats.grad_norms["temperature"] = temp_grad + stats.extra["temperature"] = self.temperature + + self._update_target_networks() + self._optimization_step += 1 + return stats + + def _compute_loss_critic(self, batch: dict[str, Any]) -> Tensor: + # Extract common components from batch + observations = batch["state"] + actions = batch[ACTION] + observation_features = batch.get("observation_feature") + # Extract critic-specific components + rewards = batch["reward"] + next_observations = batch["next_state"] + done = batch["done"] + next_observation_features = batch.get("next_observation_feature") + + with torch.no_grad(): + next_action_preds, next_log_probs, _ = self.policy.actor( + next_observations, next_observation_features + ) + + # 2- compute q targets + q_targets = self._critic_forward( + observations=next_observations, + actions=next_action_preds, + use_target=True, + observation_features=next_observation_features, + ) + + # subsample critics to prevent overfitting if use high UTD (update to date) + # TODO: Get indices before forward pass to avoid unnecessary computation + if self.config.num_subsample_critics is not None: + indices = torch.randperm(self.config.num_critics) + indices = indices[: self.config.num_subsample_critics] + q_targets = q_targets[indices] + + # critics subsample size + min_q, _ = q_targets.min(dim=0) # Get values from min operation + if self.config.use_backup_entropy: + min_q = min_q - (self.temperature * next_log_probs) + + td_target = rewards + (1 - done) * self.config.discount * min_q + + # 3- compute predicted qs + if self.policy_config.num_discrete_actions is not None: + # NOTE: We only want to keep the continuous action part + # In the buffer we have the full action space (continuous + discrete) + # We need to split them before concatenating them in the critic forward + actions: Tensor = actions[:, :DISCRETE_DIMENSION_INDEX] + q_preds = self._critic_forward( + observations=observations, + actions=actions, + use_target=False, + observation_features=observation_features, + ) + + # 4- Calculate loss + # Compute state-action value loss (TD loss) for all of the Q functions in the ensemble. + td_target_duplicate = einops.repeat(td_target, "b -> e b", e=q_preds.shape[0]) + # You compute the mean loss of the batch for each critic and then to compute the final loss you sum them up + critics_loss = ( + F.mse_loss( + input=q_preds, + target=td_target_duplicate, + reduction="none", + ).mean(dim=1) + ).sum() + return critics_loss + + def _compute_loss_discrete_critic(self, batch: dict[str, Any]) -> Tensor: + observations = batch["state"] + actions = batch[ACTION] + rewards = batch["reward"] + next_observations = batch["next_state"] + done = batch["done"] + observation_features = batch.get("observation_feature") + next_observation_features = batch.get("next_observation_feature") + complementary_info = batch.get("complementary_info") + + # NOTE: We only want to keep the discrete action part + # In the buffer we have the full action space (continuous + discrete) + # We need to split them before concatenating them in the critic forward + actions_discrete: Tensor = actions[:, DISCRETE_DIMENSION_INDEX:].clone() + actions_discrete = torch.round(actions_discrete) + actions_discrete = actions_discrete.long() + + discrete_penalties: Tensor | None = None + if complementary_info is not None: + discrete_penalties = complementary_info.get("discrete_penalty") + + with torch.no_grad(): + # For DQN, select actions using online network, evaluate with target network + next_discrete_qs = self._discrete_critic_forward( + next_observations, use_target=False, observation_features=next_observation_features + ) + best_next_discrete_action = torch.argmax(next_discrete_qs, dim=-1, keepdim=True) + + # Get target Q-values from target network + target_next_discrete_qs = self._discrete_critic_forward( + observations=next_observations, + use_target=True, + observation_features=next_observation_features, + ) + + # Use gather to select Q-values for best actions + target_next_discrete_q = torch.gather( + target_next_discrete_qs, dim=1, index=best_next_discrete_action + ).squeeze(-1) + + # Compute target Q-value with Bellman equation + rewards_discrete = rewards + if discrete_penalties is not None: + rewards_discrete = rewards + discrete_penalties + target_discrete_q = rewards_discrete + (1 - done) * self.config.discount * target_next_discrete_q + + # Get predicted Q-values for current observations + predicted_discrete_qs = self._discrete_critic_forward( + observations=observations, use_target=False, observation_features=observation_features + ) + + # Use gather to select Q-values for taken actions + predicted_discrete_q = torch.gather(predicted_discrete_qs, dim=1, index=actions_discrete).squeeze(-1) + + # Compute MSE loss between predicted and target Q-values + discrete_critic_loss = F.mse_loss(input=predicted_discrete_q, target=target_discrete_q) + return discrete_critic_loss + + def _compute_loss_actor(self, batch: dict[str, Any]) -> Tensor: + observations = batch["state"] + observation_features = batch.get("observation_feature") + + actions_pi, log_probs, _ = self.policy.actor(observations, observation_features) + + q_preds = self._critic_forward( + observations=observations, + actions=actions_pi, + use_target=False, + observation_features=observation_features, + ) + min_q_preds = q_preds.min(dim=0)[0] + + actor_loss = ((self.temperature * log_probs) - min_q_preds).mean() + return actor_loss + + def _compute_loss_temperature(self, batch: dict[str, Any]) -> Tensor: + """Compute the temperature loss""" + observations = batch["state"] + observation_features = batch.get("observation_feature") + + # calculate temperature loss + with torch.no_grad(): + _, log_probs, _ = self.policy.actor(observations, observation_features) + + temperature_loss = (-self.log_alpha.exp() * (log_probs + self.target_entropy)).mean() + return temperature_loss + + def _update_target_networks(self) -> None: + """Update target networks with exponential moving average""" + for target_p, p in zip( + self.critic_target.parameters(), self.critic_ensemble.parameters(), strict=True + ): + target_p.data.copy_( + p.data * self.config.critic_target_update_weight + + target_p.data * (1.0 - self.config.critic_target_update_weight) + ) + if self.policy_config.num_discrete_actions is not None: + for target_p, p in zip( + self.discrete_critic_target.parameters(), + self.policy.discrete_critic.parameters(), + strict=True, + ): + target_p.data.copy_( + p.data * self.config.critic_target_update_weight + + target_p.data * (1.0 - self.config.critic_target_update_weight) + ) + + def _prepare_forward_batch( + self, batch: BatchType, *, include_complementary_info: bool = True + ) -> dict[str, Any]: + observations = batch["state"] + next_observations = batch["next_state"] + observation_features, next_observation_features = self.get_observation_features( + observations, next_observations + ) + forward_batch: dict[str, Any] = { + ACTION: batch[ACTION], + "reward": batch["reward"], + "state": observations, + "next_state": next_observations, + "done": batch["done"], + "observation_feature": observation_features, + "next_observation_feature": next_observation_features, + } + if include_complementary_info and "complementary_info" in batch: + forward_batch["complementary_info"] = batch["complementary_info"] + return forward_batch + + def make_optimizers_and_scheduler(self) -> dict[str, Optimizer]: + """ + Creates and returns optimizers for the actor, critic, and temperature components of a reinforcement learning policy. + + This function sets up Adam optimizers for: + - The **actor network**, ensuring that only relevant parameters are optimized. + - The **critic ensemble**, which evaluates the value function. + - The **temperature parameter**, which controls the entropy in soft actor-critic (SAC)-like methods. + + It also initializes a learning rate scheduler, though currently, it is set to `None`. + + NOTE: + - If the encoder is shared, its parameters are excluded from the actor's optimization process. + - The policy's log temperature (`log_alpha`) is wrapped in a list to ensure proper optimization as a standalone tensor. + + Args: + cfg: Configuration object containing hyperparameters. + policy (nn.Module): The policy model containing the actor, critic, and temperature components. + + Returns: + A dictionary mapping component names ("actor", "critic", "temperature") + to their respective Adam optimizers. + """ + actor_params = self.policy.get_optim_params()["actor"] + self.optimizers = { + "actor": torch.optim.Adam(actor_params, lr=self.config.actor_lr), + "critic": torch.optim.Adam(self.critic_ensemble.parameters(), lr=self.config.critic_lr), + "temperature": torch.optim.Adam([self.log_alpha], lr=self.config.temperature_lr), + } + if self.policy_config.num_discrete_actions is not None: + self.optimizers["discrete_critic"] = torch.optim.Adam( + self.policy.discrete_critic.parameters(), lr=self.config.critic_lr + ) + return self.optimizers + + def get_optimizers(self) -> dict[str, Optimizer]: + return self.optimizers + + def get_weights(self) -> dict[str, Any]: + """Send actor + discrete-critic state dicts.""" + state_dicts: dict[str, Any] = { + "policy": move_state_dict_to_device(self.policy.actor.state_dict(), device="cpu"), + } + if self.policy_config.num_discrete_actions is not None: + state_dicts["discrete_critic"] = move_state_dict_to_device( + self.policy.discrete_critic.state_dict(), device="cpu" + ) + return state_dicts + + def load_weights(self, weights: dict[str, Any], device: str | torch.device = "cpu") -> None: + """Load actor + discrete-critic weights into the policy.""" + actor_sd = move_state_dict_to_device(weights["policy"], device=device) + self.policy.actor.load_state_dict(actor_sd) + if "discrete_critic" in weights and self.policy.discrete_critic is not None: + discrete_sd = move_state_dict_to_device(weights["discrete_critic"], device=device) + self.policy.discrete_critic.load_state_dict(discrete_sd) + + def state_dict(self) -> dict[str, torch.Tensor]: + """Algorithm-owned trainable tensors. + + Encoder weights are stripped because they are owned by the policy + (``policy.encoder_critic``) and already saved via ``policy.save_pretrained``. + """ + bundle: dict[str, torch.Tensor] = {} + for k, v in _strip_encoder_keys(self.critic_ensemble.state_dict()).items(): + bundle[f"critic_ensemble.{k}"] = v + for k, v in _strip_encoder_keys(self.critic_target.state_dict()).items(): + bundle[f"critic_target.{k}"] = v + if self.discrete_critic_target is not None: + for k, v in _strip_encoder_keys(self.discrete_critic_target.state_dict()).items(): + bundle[f"discrete_critic_target.{k}"] = v + bundle["log_alpha"] = self.log_alpha.detach() + return bundle + + def load_state_dict( + self, + state_dict: dict[str, torch.Tensor], + device: str | torch.device = "cpu", + ) -> None: + """In-place load of algorithm-owned tensors. + + ``log_alpha`` is restored via ``Parameter.data.copy_`` so the + ``temperature`` optimizer's reference to the parameter object stays + valid after resume. + """ + critic_ensemble_state = _split_prefix(state_dict, "critic_ensemble.") + critic_target_state = _split_prefix(state_dict, "critic_target.") + self.critic_ensemble.load_state_dict(critic_ensemble_state, strict=False) + self.critic_target.load_state_dict(critic_target_state, strict=False) + + if self.discrete_critic_target is not None: + discrete_target_state = _split_prefix(state_dict, "discrete_critic_target.") + self.discrete_critic_target.load_state_dict(discrete_target_state, strict=False) + + if "log_alpha" in state_dict: + self.log_alpha.data.copy_(state_dict["log_alpha"].to(self.log_alpha.device)) + + def get_observation_features( + self, observations: Tensor, next_observations: Tensor + ) -> tuple[Tensor | None, Tensor | None]: + """ + Get observation features from the policy encoder. It act as cache for the observation features. + when the encoder is frozen, the observation features are not updated. + We can save compute by caching the observation features. + + Args: + policy: The policy model + observations: The current observations + next_observations: The next observations + + Returns: + tuple: observation_features, next_observation_features + """ + + if self.policy.config.vision_encoder_name is None or not self.policy.config.freeze_vision_encoder: + return None, None + + with torch.no_grad(): + observation_features = self.policy.actor.encoder.get_cached_image_features(observations) + next_observation_features = self.policy.actor.encoder.get_cached_image_features(next_observations) + + return observation_features, next_observation_features + + +def _strip_encoder_keys(state: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: + """Drop ``encoder.*`` keys from a critic-module state dict.""" + return {k: v for k, v in state.items() if not k.startswith("encoder.")} + + +def _split_prefix(state: dict[str, torch.Tensor], prefix: str) -> dict[str, torch.Tensor]: + """Return the subset of ``state`` whose keys start with ``prefix``, prefix-stripped.""" + return {k.removeprefix(prefix): v for k, v in state.items() if k.startswith(prefix)} + + +class CriticHead(nn.Module): + def __init__( + self, + input_dim: int, + hidden_dims: list[int], + activations: Callable[[torch.Tensor], torch.Tensor] | str = nn.SiLU(), + activate_final: bool = False, + dropout_rate: float | None = None, + init_final: float | None = None, + final_activation: Callable[[torch.Tensor], torch.Tensor] | str | None = None, + ): + super().__init__() + self.net = MLP( + input_dim=input_dim, + hidden_dims=hidden_dims, + activations=activations, + activate_final=activate_final, + dropout_rate=dropout_rate, + final_activation=final_activation, + ) + self.output_layer = nn.Linear(in_features=hidden_dims[-1], out_features=1) + if init_final is not None: + nn.init.uniform_(self.output_layer.weight, -init_final, init_final) + nn.init.uniform_(self.output_layer.bias, -init_final, init_final) + else: + orthogonal_init()(self.output_layer.weight) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.output_layer(self.net(x)) + + +class CriticEnsemble(nn.Module): + """ + CriticEnsemble wraps multiple CriticHead modules into an ensemble. + + Args: + encoder (GaussianActorObservationEncoder): encoder for observations. + ensemble (List[CriticHead]): list of critic heads. + init_final (float | None): optional initializer scale for final layers. + + Forward returns a tensor of shape (num_critics, batch_size) containing Q-values. + """ + + def __init__( + self, + encoder: GaussianActorObservationEncoder, + ensemble: list[CriticHead], + init_final: float | None = None, + ): + super().__init__() + self.encoder = encoder + self.init_final = init_final + self.critics = nn.ModuleList(ensemble) + + def forward( + self, + observations: dict[str, torch.Tensor], + actions: torch.Tensor, + observation_features: torch.Tensor | None = None, + ) -> torch.Tensor: + device = get_device_from_parameters(self) + # Move each tensor in observations to device + observations = {k: v.to(device) for k, v in observations.items()} + + obs_enc = self.encoder(observations, cache=observation_features) + + inputs = torch.cat([obs_enc, actions], dim=-1) + + # Loop through critics and collect outputs + q_values = [] + for critic in self.critics: + q_values.append(critic(inputs)) + + # Stack outputs to match expected shape [num_critics, batch_size] + q_values = torch.stack([q.squeeze(-1) for q in q_values], dim=0) + return q_values diff --git a/src/lerobot/rl/buffer.py b/src/lerobot/rl/buffer.py index 05b8419bd..cec80b723 100644 --- a/src/lerobot/rl/buffer.py +++ b/src/lerobot/rl/buffer.py @@ -97,8 +97,8 @@ class ReplayBuffer: Args: capacity (int): Maximum number of transitions to store in the buffer. device (str): The device where the tensors will be moved when sampling ("cuda:0" or "cpu"). - state_keys (List[str]): The list of keys that appear in `state` and `next_state`. - image_augmentation_function (Optional[Callable]): A function that takes a batch of images + state_keys (list[str]): The list of keys that appear in `state` and `next_state`. + image_augmentation_function (Callable | None): A function that takes a batch of images and returns a batch of augmented images. If None, a default augmentation function is used. use_drq (bool): Whether to use the default DRQ image augmentation style, when sampling in the buffer. storage_device: The device (e.g. "cpu" or "cuda:0") where the data will be stored. @@ -634,7 +634,7 @@ class ReplayBuffer: If None, you must handle or define default keys. Returns: - transitions (List[Transition]): + transitions (list[Transition]): A list of Transition dictionaries with the same length as `dataset`. """ if state_keys is None: diff --git a/src/lerobot/rl/crop_dataset_roi.py b/src/lerobot/rl/crop_dataset_roi.py index cc808bcb0..eece13a4c 100644 --- a/src/lerobot/rl/crop_dataset_roi.py +++ b/src/lerobot/rl/crop_dataset_roi.py @@ -176,11 +176,11 @@ def convert_lerobot_dataset_to_cropped_lerobot_dataset( Args: original_dataset (LeRobotDataset): The source dataset. - crop_params_dict (Dict[str, Tuple[int, int, int, int]]): + crop_params_dict (dict[str, Tuple[int, int, int, int]]): A dictionary mapping observation keys to crop parameters (top, left, height, width). new_repo_id (str): Repository id for the new dataset. new_dataset_root (str): The root directory where the new dataset will be written. - resize_size (Tuple[int, int], optional): The target size (height, width) after cropping. + resize_size (tuple[int, int], optional): The target size (height, width) after cropping. Defaults to (128, 128). Returns: diff --git a/src/lerobot/rl/data_sources/__init__.py b/src/lerobot/rl/data_sources/__init__.py new file mode 100644 index 000000000..97cfe5001 --- /dev/null +++ b/src/lerobot/rl/data_sources/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from lerobot.types import BatchType + +from .data_mixer import DataMixer, OnlineOfflineMixer + +__all__ = ["BatchType", "DataMixer", "OnlineOfflineMixer"] diff --git a/src/lerobot/rl/data_sources/data_mixer.py b/src/lerobot/rl/data_sources/data_mixer.py new file mode 100644 index 000000000..57a2d86be --- /dev/null +++ b/src/lerobot/rl/data_sources/data_mixer.py @@ -0,0 +1,97 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import abc + +from lerobot.types import BatchType + +from ..buffer import ReplayBuffer, concatenate_batch_transitions + + +class DataMixer(abc.ABC): + """Abstract interface for all data mixing strategies.""" + + @abc.abstractmethod + def sample(self, batch_size: int) -> BatchType: + """Draw one batch of ``batch_size`` transitions.""" + raise NotImplementedError + + def get_iterator( + self, + batch_size: int, + async_prefetch: bool = True, + queue_size: int = 2, + ): + """Infinite iterator that yields batches.""" + while True: + yield self.sample(batch_size) + + +class OnlineOfflineMixer(DataMixer): + """Mixes transitions from an online and an offline replay buffer.""" + + def __init__( + self, + online_buffer: ReplayBuffer, + offline_buffer: ReplayBuffer | None = None, + online_ratio: float = 1.0, + ): + if not 0.0 <= online_ratio <= 1.0: + raise ValueError(f"online_ratio must be in [0, 1], got {online_ratio}") + self.online_buffer = online_buffer + self.offline_buffer = offline_buffer + self.online_ratio = online_ratio + + def sample(self, batch_size: int) -> BatchType: + if self.offline_buffer is None: + return self.online_buffer.sample(batch_size) + + n_online = max(1, int(batch_size * self.online_ratio)) + n_offline = batch_size - n_online + + online_batch = self.online_buffer.sample(n_online) + offline_batch = self.offline_buffer.sample(n_offline) + return concatenate_batch_transitions(online_batch, offline_batch) + + def get_iterator( + self, + batch_size: int, + async_prefetch: bool = True, + queue_size: int = 2, + ): + """Yield batches by composing buffer async iterators.""" + + n_online = max(1, int(batch_size * self.online_ratio)) + + online_iter = self.online_buffer.get_iterator( + batch_size=n_online, + async_prefetch=async_prefetch, + queue_size=queue_size, + ) + + if self.offline_buffer is None: + yield from online_iter + return + + n_offline = batch_size - n_online + offline_iter = self.offline_buffer.get_iterator( + batch_size=n_offline, + async_prefetch=async_prefetch, + queue_size=queue_size, + ) + + while True: + yield concatenate_batch_transitions(next(online_iter), next(offline_iter)) diff --git a/src/lerobot/rl/eval_policy.py b/src/lerobot/rl/eval_policy.py index 4398351c5..0f42d7573 100644 --- a/src/lerobot/rl/eval_policy.py +++ b/src/lerobot/rl/eval_policy.py @@ -17,7 +17,6 @@ import logging from lerobot.cameras import opencv # noqa: F401 from lerobot.configs import parser -from lerobot.configs.train import TrainRLServerPipelineConfig from lerobot.datasets import LeRobotDataset from lerobot.policies import make_policy from lerobot.robots import ( # noqa: F401 @@ -31,6 +30,7 @@ from lerobot.teleoperators import ( ) from .gym_manipulator import make_robot_env +from .train_rl import TrainRLServerPipelineConfig logging.basicConfig(level=logging.INFO) diff --git a/src/lerobot/rl/gym_manipulator.py b/src/lerobot/rl/gym_manipulator.py index 2190070f5..03f7b4eea 100644 --- a/src/lerobot/rl/gym_manipulator.py +++ b/src/lerobot/rl/gym_manipulator.py @@ -74,6 +74,7 @@ from lerobot.teleoperators import ( from lerobot.teleoperators.teleoperator import Teleoperator from lerobot.teleoperators.utils import TeleopEvents from lerobot.utils.constants import ACTION, DONE, OBS_IMAGES, OBS_STATE, REWARD +from lerobot.utils.import_utils import require_package from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.utils import log_say @@ -312,6 +313,7 @@ def make_robot_env(cfg: HILSerlRobotEnvConfig) -> tuple[gym.Env, Any]: # Check if this is a GymHIL simulation environment if cfg.name == "gym_hil": assert cfg.robot is None and cfg.teleop is None, "GymHIL environment does not support robot or teleop" + require_package("gym-hil", extra="hilserl", import_name="gym_hil") import gym_hil # noqa: F401 # Extract gripper settings with defaults @@ -383,10 +385,21 @@ def make_processors( GymHILAdapterProcessorStep(), Numpy2TorchActionProcessorStep(), VanillaObservationProcessorStep(), - AddBatchDimensionProcessorStep(), - DeviceProcessorStep(device=device), ] + # Add time limit processor if reset config exists + if cfg.processor.reset is not None: + env_pipeline_steps.append( + TimeLimitProcessorStep(max_episode_steps=int(cfg.processor.reset.control_time_s * cfg.fps)) + ) + + env_pipeline_steps.extend( + [ + AddBatchDimensionProcessorStep(), + DeviceProcessorStep(device=device), + ] + ) + return DataProcessorPipeline( steps=env_pipeline_steps, to_transition=identity_transition, to_output=identity_transition ), DataProcessorPipeline( @@ -551,8 +564,19 @@ def step_env_and_process_transition( terminated = terminated or processed_action_transition[TransitionKey.DONE] truncated = truncated or processed_action_transition[TransitionKey.TRUNCATED] complementary_data = processed_action_transition[TransitionKey.COMPLEMENTARY_DATA].copy() + + if hasattr(env, "get_raw_joint_positions"): + raw_joint_positions = env.get_raw_joint_positions() + if raw_joint_positions is not None: + complementary_data["raw_joint_positions"] = raw_joint_positions + + # Merge env and action-processor info: env wins for str keys, action-processor + # wins for `TeleopEvents` enum keys + action_info = processed_action_transition[TransitionKey.INFO] new_info = info.copy() - new_info.update(processed_action_transition[TransitionKey.INFO]) + for key, value in action_info.items(): + if isinstance(key, TeleopEvents): + new_info[key] = value new_transition = create_transition( observation=obs, @@ -568,6 +592,24 @@ def step_env_and_process_transition( return new_transition +def reset_and_build_transition( + env: gym.Env, + env_processor: DataProcessorPipeline[EnvTransition, EnvTransition], + action_processor: DataProcessorPipeline[EnvTransition, EnvTransition], +) -> EnvTransition: + """Reset env + processors and return the first env-processed transition.""" + obs, info = env.reset() + env_processor.reset() + action_processor.reset() + complementary_data: dict[str, Any] = {} + if hasattr(env, "get_raw_joint_positions"): + raw_joint_positions = env.get_raw_joint_positions() + if raw_joint_positions is not None: + complementary_data["raw_joint_positions"] = raw_joint_positions + transition = create_transition(observation=obs, info=info, complementary_data=complementary_data) + return env_processor(data=transition) + + def control_loop( env: gym.Env, env_processor: DataProcessorPipeline[EnvTransition, EnvTransition], @@ -593,17 +635,7 @@ def control_loop( print("- When not intervening, robot will stay still") print("- Press Ctrl+C to exit") - # Reset environment and processors - obs, info = env.reset() - complementary_data = ( - {"raw_joint_positions": info.pop("raw_joint_positions")} if "raw_joint_positions" in info else {} - ) - env_processor.reset() - action_processor.reset() - - # Process initial observation - transition = create_transition(observation=obs, info=info, complementary_data=complementary_data) - transition = env_processor(data=transition) + transition = reset_and_build_transition(env, env_processor, action_processor) # Determine if gripper is used use_gripper = cfg.env.processor.gripper.use_gripper if cfg.env.processor.gripper is not None else True @@ -659,79 +691,82 @@ def control_loop( episode_step = 0 episode_start_time = time.perf_counter() - while episode_idx < cfg.dataset.num_episodes_to_record: - step_start_time = time.perf_counter() + try: + while episode_idx < cfg.dataset.num_episodes_to_record: + step_start_time = time.perf_counter() - # Create a neutral action (no movement) - neutral_action = torch.tensor([0.0, 0.0, 0.0], dtype=torch.float32) - if use_gripper: - neutral_action = torch.cat([neutral_action, torch.tensor([0.0])]) # Gripper stay + # Create a neutral action (no movement) + neutral_action = torch.tensor([0.0, 0.0, 0.0], dtype=torch.float32) + if use_gripper: + neutral_action = torch.cat([neutral_action, torch.tensor([1.0])]) # Gripper stay - # Use the new step function - transition = step_env_and_process_transition( - env=env, - transition=transition, - action=neutral_action, - env_processor=env_processor, - action_processor=action_processor, - ) - terminated = transition.get(TransitionKey.DONE, False) - truncated = transition.get(TransitionKey.TRUNCATED, False) - - if cfg.mode == "record": - observations = { + observation = { k: v.squeeze(0).cpu() for k, v in transition[TransitionKey.OBSERVATION].items() if isinstance(v, torch.Tensor) } - # Use teleop_action if available, otherwise use the action from the transition - action_to_record = transition[TransitionKey.COMPLEMENTARY_DATA].get( - "teleop_action", transition[TransitionKey.ACTION] + + transition = step_env_and_process_transition( + env=env, + transition=transition, + action=neutral_action, + env_processor=env_processor, + action_processor=action_processor, ) - frame = { - **observations, - ACTION: action_to_record.cpu(), - REWARD: np.array([transition[TransitionKey.REWARD]], dtype=np.float32), - DONE: np.array([terminated or truncated], dtype=bool), - } - if use_gripper: - discrete_penalty = transition[TransitionKey.COMPLEMENTARY_DATA].get("discrete_penalty", 0.0) - frame["complementary_info.discrete_penalty"] = np.array([discrete_penalty], dtype=np.float32) + terminated = transition.get(TransitionKey.DONE, False) + truncated = transition.get(TransitionKey.TRUNCATED, False) - if dataset is not None: - frame["task"] = cfg.dataset.task - dataset.add_frame(frame) + if cfg.mode == "record": + action_to_record = transition[TransitionKey.COMPLEMENTARY_DATA].get( + "teleop_action", transition[TransitionKey.ACTION] + ) + frame = { + **observation, + ACTION: action_to_record.cpu(), + REWARD: np.array([transition[TransitionKey.REWARD]], dtype=np.float32), + DONE: np.array([terminated or truncated], dtype=bool), + } + if use_gripper: + discrete_penalty = transition[TransitionKey.COMPLEMENTARY_DATA].get( + "discrete_penalty", 0.0 + ) + frame["complementary_info.discrete_penalty"] = np.array( + [discrete_penalty], dtype=np.float32 + ) - episode_step += 1 + if dataset is not None: + frame["task"] = cfg.dataset.task + dataset.add_frame(frame) - # Handle episode termination - if terminated or truncated: - episode_time = time.perf_counter() - episode_start_time - logging.info( - f"Episode ended after {episode_step} steps in {episode_time:.1f}s with reward {transition[TransitionKey.REWARD]}" - ) - episode_step = 0 - episode_idx += 1 + episode_step += 1 - if dataset is not None: - if transition[TransitionKey.INFO].get(TeleopEvents.RERECORD_EPISODE, False): - logging.info(f"Re-recording episode {episode_idx}") - dataset.clear_episode_buffer() - episode_idx -= 1 - else: - logging.info(f"Saving episode {episode_idx}") - dataset.save_episode() + # Handle episode termination + if terminated or truncated: + episode_time = time.perf_counter() - episode_start_time + logging.info( + f"Episode ended after {episode_step} steps in {episode_time:.1f}s with reward {transition[TransitionKey.REWARD]}" + ) + episode_step = 0 + episode_idx += 1 - # Reset for new episode - obs, info = env.reset() - env_processor.reset() - action_processor.reset() + if dataset is not None: + if transition[TransitionKey.INFO].get(TeleopEvents.RERECORD_EPISODE, False): + logging.info(f"Re-recording episode {episode_idx}") + dataset.clear_episode_buffer() + episode_idx -= 1 + else: + logging.info(f"Saving episode {episode_idx}") + dataset.save_episode() - transition = create_transition(observation=obs, info=info) - transition = env_processor(transition) + # Reset for new episode + transition = reset_and_build_transition(env, env_processor, action_processor) - # Maintain fps timing - precise_sleep(max(dt - (time.perf_counter() - step_start_time), 0.0)) + # Maintain fps timing + precise_sleep(max(dt - (time.perf_counter() - step_start_time), 0.0)) + finally: + if dataset is not None and dataset.writer is not None and dataset.writer.image_writer is not None: + logging.info("Waiting for image writer to finish...") + dataset.writer.image_writer.stop() if dataset is not None and cfg.dataset.push_to_hub: logging.info("Finalizing dataset before pushing to hub") diff --git a/src/lerobot/rl/learner.py b/src/lerobot/rl/learner.py index 14542576d..41cfd8c03 100644 --- a/src/lerobot/rl/learner.py +++ b/src/lerobot/rl/learner.py @@ -51,9 +51,21 @@ import time from concurrent.futures import ThreadPoolExecutor from pathlib import Path from pprint import pformat +from typing import TYPE_CHECKING, Any + +from lerobot.utils.import_utils import _grpc_available, require_package + +if TYPE_CHECKING or _grpc_available: + import grpc + + from lerobot.transport import services_pb2_grpc +else: + grpc = None + services_pb2_grpc = None -import grpc import torch +from huggingface_hub.constants import SAFETENSORS_SINGLE_FILE +from safetensors.torch import load_file as load_safetensors from termcolor import colored from torch import nn from torch.multiprocessing import Queue @@ -68,14 +80,11 @@ from lerobot.common.train_utils import ( ) from lerobot.common.wandb_utils import WandBLogger from lerobot.configs import parser -from lerobot.configs.train import TrainRLServerPipelineConfig from lerobot.datasets import LeRobotDataset, make_dataset -from lerobot.policies import make_policy -from lerobot.policies.sac.modeling_sac import SACPolicy +from lerobot.policies import make_policy, make_pre_post_processors from lerobot.robots import so_follower # noqa: F401 from lerobot.teleoperators import gamepad, so_leader # noqa: F401 from lerobot.teleoperators.utils import TeleopEvents -from lerobot.transport import services_pb2_grpc from lerobot.transport.utils import ( MAX_MESSAGE_SIZE, bytes_to_python_object, @@ -84,26 +93,35 @@ from lerobot.transport.utils import ( ) from lerobot.utils.constants import ( ACTION, + ALGORITHM_DIR, CHECKPOINTS_DIR, LAST_CHECKPOINT_LINK, PRETRAINED_MODEL_DIR, TRAINING_STATE_DIR, + TRAINING_STEP, ) from lerobot.utils.device_utils import get_safe_torch_device +from lerobot.utils.io_utils import load_json, write_json from lerobot.utils.process import ProcessSignalHandler from lerobot.utils.random_utils import set_seed -from lerobot.utils.transition import move_state_dict_to_device, move_transition_to_device from lerobot.utils.utils import ( format_big_number, init_logging, ) -from .buffer import ReplayBuffer, concatenate_batch_transitions +from .algorithms.base import RLAlgorithm +from .algorithms.factory import make_algorithm +from .buffer import ReplayBuffer +from .data_sources import OnlineOfflineMixer from .learner_service import MAX_WORKERS, SHUTDOWN_TIMEOUT, LearnerService +from .train_rl import TrainRLServerPipelineConfig +from .trainer import RLTrainer @parser.wrap() def train_cli(cfg: TrainRLServerPipelineConfig): + # Fail fast with a friendly error if the optional ``hilserl`` extra is missing. + require_package("grpcio", extra="hilserl", import_name="grpc") if not use_threads(cfg): import torch.multiprocessing as mp @@ -179,7 +197,7 @@ def train(cfg: TrainRLServerPipelineConfig, job_name: str | None = None): def start_learner_threads( cfg: TrainRLServerPipelineConfig, wandb_logger: WandBLogger | None, - shutdown_event: any, # Event, + shutdown_event: Any, # Event ) -> None: """ Start the learner threads for training. @@ -253,7 +271,7 @@ def start_learner_threads( def add_actor_information_and_train( cfg: TrainRLServerPipelineConfig, wandb_logger: WandBLogger | None, - shutdown_event: any, # Event, + shutdown_event: Any, # Event transition_queue: Queue, interaction_message_queue: Queue, parameters_queue: Queue, @@ -266,8 +284,8 @@ def add_actor_information_and_train( - Transfers transitions from the actor to the replay buffer. - Logs received interaction messages. - Ensures training begins only when the replay buffer has a sufficient number of transitions. - - Samples batches from the replay buffer and performs multiple critic updates. - - Periodically updates the actor, critic, and temperature optimizers. + - Delegates training updates to an ``RLAlgorithm``. + - Periodically pushes updated weights to actors. - Logs training statistics, including loss values and optimization frequency. NOTE: This function doesn't have a single responsibility, it should be split into multiple functions @@ -286,17 +304,13 @@ def add_actor_information_and_train( # of 7% device = get_safe_torch_device(try_device=cfg.policy.device, log=True) storage_device = get_safe_torch_device(try_device=cfg.policy.storage_device) - clip_grad_norm_value = cfg.policy.grad_clip_norm online_step_before_learning = cfg.policy.online_step_before_learning - utd_ratio = cfg.policy.utd_ratio fps = cfg.env.fps log_freq = cfg.log_freq save_freq = cfg.save_freq - policy_update_freq = cfg.policy.policy_update_freq policy_parameters_push_frequency = cfg.policy.actor_learner_config.policy_parameters_push_frequency saving_checkpoint = cfg.save_checkpoint online_steps = cfg.policy.online_steps - async_prefetch = cfg.policy.async_prefetch # Initialize logging for multiprocessing if not use_threads(cfg): @@ -308,7 +322,7 @@ def add_actor_information_and_train( logging.info("Initializing policy") - policy: SACPolicy = make_policy( + policy = make_policy( cfg=cfg.policy, env_cfg=cfg.env, ) @@ -317,15 +331,17 @@ def add_actor_information_and_train( policy.train() - push_actor_policy_to_queue(parameters_queue=parameters_queue, policy=policy) + algorithm = make_algorithm(cfg=cfg.algorithm, policy=policy) + preprocessor, postprocessor = make_pre_post_processors( + policy_cfg=cfg.policy, + dataset_stats=cfg.policy.dataset_stats, + ) + + # Push initial policy weights to actors + push_actor_policy_to_queue(parameters_queue=parameters_queue, algorithm=algorithm) last_time_policy_pushed = time.time() - optimizers, lr_scheduler = make_optimizers_and_scheduler(cfg=cfg, policy=policy) - - # If we are resuming, we need to load the training state - resume_optimization_step, resume_interaction_step = load_training_state(cfg=cfg, optimizers=optimizers) - log_training_info(cfg=cfg, policy=policy) replay_buffer = initialize_replay_buffer(cfg, device, storage_device) @@ -338,21 +354,37 @@ def add_actor_information_and_train( device=device, storage_device=storage_device, ) - batch_size: int = batch_size // 2 # We will sample from both replay buffer + + # DataMixer: online-only or online/offline 50-50 mix + data_mixer = OnlineOfflineMixer( + online_buffer=replay_buffer, + offline_buffer=offline_replay_buffer, + online_ratio=cfg.online_ratio, + ) + # RLTrainer owns the iterator, preprocessor, and creates optimizers. + trainer = RLTrainer( + algorithm=algorithm, + data_mixer=data_mixer, + batch_size=batch_size, + preprocessor=preprocessor, + ) + + # If we are resuming, we need to load the training state + optimizers = algorithm.get_optimizers() + resume_optimization_step, resume_interaction_step = load_training_state( + cfg=cfg, optimizers=optimizers, algorithm=algorithm, device=device + ) logging.info("Starting learner thread") interaction_message = None optimization_step = resume_optimization_step if resume_optimization_step is not None else 0 + algorithm.optimization_step = optimization_step interaction_step_shift = resume_interaction_step if resume_interaction_step is not None else 0 dataset_repo_id = None if cfg.dataset is not None: dataset_repo_id = cfg.dataset.repo_id - # Initialize iterators - online_iterator = None - offline_iterator = None - # NOTE: THIS IS THE MAIN LOOP OF THE LEARNER while True: # Exit the training loop if shutdown is requested @@ -365,7 +397,6 @@ def add_actor_information_and_train( transition_queue=transition_queue, replay_buffer=replay_buffer, offline_replay_buffer=offline_replay_buffer, - device=device, dataset_repo_id=dataset_repo_id, shutdown_event=shutdown_event, ) @@ -382,180 +413,20 @@ def add_actor_information_and_train( if len(replay_buffer) < online_step_before_learning: continue - if online_iterator is None: - online_iterator = replay_buffer.get_iterator( - batch_size=batch_size, async_prefetch=async_prefetch, queue_size=2 - ) - - if offline_replay_buffer is not None and offline_iterator is None: - offline_iterator = offline_replay_buffer.get_iterator( - batch_size=batch_size, async_prefetch=async_prefetch, queue_size=2 - ) - time_for_one_optimization_step = time.time() - for _ in range(utd_ratio - 1): - # Sample from the iterators - batch = next(online_iterator) - if dataset_repo_id is not None: - batch_offline = next(offline_iterator) - batch = concatenate_batch_transitions( - left_batch_transitions=batch, right_batch_transition=batch_offline - ) - - actions = batch[ACTION] - rewards = batch["reward"] - observations = batch["state"] - next_observations = batch["next_state"] - done = batch["done"] - check_nan_in_transition(observations=observations, actions=actions, next_state=next_observations) - - observation_features, next_observation_features = get_observation_features( - policy=policy, observations=observations, next_observations=next_observations - ) - - # Create a batch dictionary with all required elements for the forward method - forward_batch = { - ACTION: actions, - "reward": rewards, - "state": observations, - "next_state": next_observations, - "done": done, - "observation_feature": observation_features, - "next_observation_feature": next_observation_features, - "complementary_info": batch["complementary_info"], - } - - # Use the forward method for critic loss - critic_output = policy.forward(forward_batch, model="critic") - - # Main critic optimization - loss_critic = critic_output["loss_critic"] - optimizers["critic"].zero_grad() - loss_critic.backward() - critic_grad_norm = torch.nn.utils.clip_grad_norm_( - parameters=policy.critic_ensemble.parameters(), max_norm=clip_grad_norm_value - ) - optimizers["critic"].step() - - # Discrete critic optimization (if available) - if policy.config.num_discrete_actions is not None: - discrete_critic_output = policy.forward(forward_batch, model="discrete_critic") - loss_discrete_critic = discrete_critic_output["loss_discrete_critic"] - optimizers["discrete_critic"].zero_grad() - loss_discrete_critic.backward() - discrete_critic_grad_norm = torch.nn.utils.clip_grad_norm_( - parameters=policy.discrete_critic.parameters(), max_norm=clip_grad_norm_value - ) - optimizers["discrete_critic"].step() - - # Update target networks (main and discrete) - policy.update_target_networks() - - # Sample for the last update in the UTD ratio - batch = next(online_iterator) - - if dataset_repo_id is not None: - batch_offline = next(offline_iterator) - batch = concatenate_batch_transitions( - left_batch_transitions=batch, right_batch_transition=batch_offline - ) - - actions = batch[ACTION] - rewards = batch["reward"] - observations = batch["state"] - next_observations = batch["next_state"] - done = batch["done"] - - check_nan_in_transition(observations=observations, actions=actions, next_state=next_observations) - - observation_features, next_observation_features = get_observation_features( - policy=policy, observations=observations, next_observations=next_observations - ) - - # Create a batch dictionary with all required elements for the forward method - forward_batch = { - ACTION: actions, - "reward": rewards, - "state": observations, - "next_state": next_observations, - "done": done, - "observation_feature": observation_features, - "next_observation_feature": next_observation_features, - } - - critic_output = policy.forward(forward_batch, model="critic") - - loss_critic = critic_output["loss_critic"] - optimizers["critic"].zero_grad() - loss_critic.backward() - critic_grad_norm = torch.nn.utils.clip_grad_norm_( - parameters=policy.critic_ensemble.parameters(), max_norm=clip_grad_norm_value - ).item() - optimizers["critic"].step() - - # Initialize training info dictionary - training_infos = { - "loss_critic": loss_critic.item(), - "critic_grad_norm": critic_grad_norm, - } - - # Discrete critic optimization (if available) - if policy.config.num_discrete_actions is not None: - discrete_critic_output = policy.forward(forward_batch, model="discrete_critic") - loss_discrete_critic = discrete_critic_output["loss_discrete_critic"] - optimizers["discrete_critic"].zero_grad() - loss_discrete_critic.backward() - discrete_critic_grad_norm = torch.nn.utils.clip_grad_norm_( - parameters=policy.discrete_critic.parameters(), max_norm=clip_grad_norm_value - ).item() - optimizers["discrete_critic"].step() - - # Add discrete critic info to training info - training_infos["loss_discrete_critic"] = loss_discrete_critic.item() - training_infos["discrete_critic_grad_norm"] = discrete_critic_grad_norm - - # Actor and temperature optimization (at specified frequency) - if optimization_step % policy_update_freq == 0: - for _ in range(policy_update_freq): - # Actor optimization - actor_output = policy.forward(forward_batch, model="actor") - loss_actor = actor_output["loss_actor"] - optimizers["actor"].zero_grad() - loss_actor.backward() - actor_grad_norm = torch.nn.utils.clip_grad_norm_( - parameters=policy.actor.parameters(), max_norm=clip_grad_norm_value - ).item() - optimizers["actor"].step() - - # Add actor info to training info - training_infos["loss_actor"] = loss_actor.item() - training_infos["actor_grad_norm"] = actor_grad_norm - - # Temperature optimization - temperature_output = policy.forward(forward_batch, model="temperature") - loss_temperature = temperature_output["loss_temperature"] - optimizers["temperature"].zero_grad() - loss_temperature.backward() - temp_grad_norm = torch.nn.utils.clip_grad_norm_( - parameters=[policy.log_alpha], max_norm=clip_grad_norm_value - ).item() - optimizers["temperature"].step() - - # Add temperature info to training info - training_infos["loss_temperature"] = loss_temperature.item() - training_infos["temperature_grad_norm"] = temp_grad_norm - training_infos["temperature"] = policy.temperature + # One training step (trainer owns data_mixer iterator; algorithm owns UTD loop) + stats = trainer.training_step() # Push policy to actors if needed if time.time() - last_time_policy_pushed > policy_parameters_push_frequency: - push_actor_policy_to_queue(parameters_queue=parameters_queue, policy=policy) + push_actor_policy_to_queue(parameters_queue=parameters_queue, algorithm=algorithm) last_time_policy_pushed = time.time() - # Update target networks (main and discrete) - policy.update_target_networks() + training_infos = stats.to_log_dict() # Log training metrics at specified intervals + optimization_step = algorithm.optimization_step if optimization_step % log_freq == 0: training_infos["replay_buffer_size"] = len(replay_buffer) if offline_replay_buffer is not None: @@ -583,7 +454,6 @@ def add_actor_information_and_train( custom_step_key="Optimization step", ) - optimization_step += 1 if optimization_step % log_freq == 0: logging.info(f"[LEARNER] Number of optimization step: {optimization_step}") @@ -597,9 +467,12 @@ def add_actor_information_and_train( policy=policy, optimizers=optimizers, replay_buffer=replay_buffer, + algorithm=algorithm, offline_replay_buffer=offline_replay_buffer, dataset_repo_id=dataset_repo_id, fps=fps, + preprocessor=preprocessor, + postprocessor=postprocessor, ) @@ -607,7 +480,7 @@ def start_learner( parameters_queue: Queue, transition_queue: Queue, interaction_message_queue: Queue, - shutdown_event: any, # Event, + shutdown_event: Any, # Event cfg: TrainRLServerPipelineConfig, ): """ @@ -681,9 +554,12 @@ def save_training_checkpoint( policy: nn.Module, optimizers: dict[str, Optimizer], replay_buffer: ReplayBuffer, + algorithm: RLAlgorithm | None = None, offline_replay_buffer: ReplayBuffer | None = None, dataset_repo_id: str | None = None, fps: int = 30, + preprocessor=None, + postprocessor=None, ) -> None: """ Save training checkpoint and associated data. @@ -707,6 +583,8 @@ def save_training_checkpoint( offline_replay_buffer: Optional offline replay buffer to save dataset_repo_id: Repository ID for dataset fps: Frames per second for dataset + preprocessor: Optional preprocessor pipeline to save + postprocessor: Optional postprocessor pipeline to save """ logging.info(f"Checkpoint policy after step {optimization_step}") _num_digits = max(6, len(str(online_steps))) @@ -715,7 +593,7 @@ def save_training_checkpoint( # Create checkpoint directory checkpoint_dir = get_step_checkpoint_dir(cfg.output_dir, online_steps, optimization_step) - # Save checkpoint + # Save policy artifacts (pretrained_model/) + Trainer scaffolding (training_state/). save_checkpoint( checkpoint_dir=checkpoint_dir, step=optimization_step, @@ -723,13 +601,22 @@ def save_training_checkpoint( policy=policy, optimizer=optimizers, scheduler=None, + preprocessor=preprocessor, + postprocessor=postprocessor, ) - # Save interaction step manually - training_state_dir = os.path.join(checkpoint_dir, TRAINING_STATE_DIR) - os.makedirs(training_state_dir, exist_ok=True) - training_state = {"step": optimization_step, "interaction_step": interaction_step} - torch.save(training_state, os.path.join(training_state_dir, "training_state.pt")) + # Algorithm-owned tensors live in their own component subfolder + # so they can be `push_to_hub`'d independently and don't bloat the inference artifact. + if algorithm is not None: + algorithm.save_pretrained(checkpoint_dir / ALGORITHM_DIR) + + # Enrich training_step.json with the RL-specific interaction_step counter so + # both can be restored from a single file. + training_state_dir = checkpoint_dir / TRAINING_STATE_DIR + write_json( + {"step": optimization_step, "interaction_step": interaction_step}, + training_state_dir / TRAINING_STEP, + ) # Update the "last" symlink update_last_checkpoint(checkpoint_dir) @@ -760,58 +647,6 @@ def save_training_checkpoint( logging.info("Resume training") -def make_optimizers_and_scheduler(cfg: TrainRLServerPipelineConfig, policy: nn.Module): - """ - Creates and returns optimizers for the actor, critic, and temperature components of a reinforcement learning policy. - - This function sets up Adam optimizers for: - - The **actor network**, ensuring that only relevant parameters are optimized. - - The **critic ensemble**, which evaluates the value function. - - The **temperature parameter**, which controls the entropy in soft actor-critic (SAC)-like methods. - - It also initializes a learning rate scheduler, though currently, it is set to `None`. - - NOTE: - - If the encoder is shared, its parameters are excluded from the actor's optimization process. - - The policy's log temperature (`log_alpha`) is wrapped in a list to ensure proper optimization as a standalone tensor. - - Args: - cfg: Configuration object containing hyperparameters. - policy (nn.Module): The policy model containing the actor, critic, and temperature components. - - Returns: - Tuple[Dict[str, torch.optim.Optimizer], Optional[torch.optim.lr_scheduler._LRScheduler]]: - A tuple containing: - - `optimizers`: A dictionary mapping component names ("actor", "critic", "temperature") to their respective Adam optimizers. - - `lr_scheduler`: Currently set to `None` but can be extended to support learning rate scheduling. - - """ - optimizer_actor = torch.optim.Adam( - params=[ - p - for n, p in policy.actor.named_parameters() - if not policy.config.shared_encoder or not n.startswith("encoder") - ], - lr=cfg.policy.actor_lr, - ) - optimizer_critic = torch.optim.Adam(params=policy.critic_ensemble.parameters(), lr=cfg.policy.critic_lr) - - if cfg.policy.num_discrete_actions is not None: - optimizer_discrete_critic = torch.optim.Adam( - params=policy.discrete_critic.parameters(), lr=cfg.policy.critic_lr - ) - optimizer_temperature = torch.optim.Adam(params=[policy.log_alpha], lr=cfg.policy.critic_lr) - lr_scheduler = None - optimizers = { - "actor": optimizer_actor, - "critic": optimizer_critic, - "temperature": optimizer_temperature, - } - if cfg.policy.num_discrete_actions is not None: - optimizers["discrete_critic"] = optimizer_discrete_critic - return optimizers, lr_scheduler - - # Training setup functions @@ -875,13 +710,20 @@ def handle_resume_logic(cfg: TrainRLServerPipelineConfig) -> TrainRLServerPipeli def load_training_state( cfg: TrainRLServerPipelineConfig, optimizers: Optimizer | dict[str, Optimizer], + algorithm: RLAlgorithm | None = None, + device: str | torch.device = "cpu", ): """ - Loads the training state (optimizers, step count, etc.) from a checkpoint. + Loads the training state (optimizers, RNG, step + interaction step, and + algorithm-owned tensors) from the most recent checkpoint. Args: - cfg (TrainRLServerPipelineConfig): Training configuration - optimizers (Optimizer | dict): Optimizers to load state into + cfg: Training configuration. + optimizers: Optimizers to load state into. + algorithm: Algorithm whose state dict should be restored. + Required for full main-equivalent resume; + the policy itself is restored separately via ``make_policy``. + device: Device on which to place loaded algorithm tensors. Returns: tuple: (optimization_step, interaction_step) or (None, None) if not resuming @@ -890,20 +732,31 @@ def load_training_state( return None, None # Construct path to the last checkpoint directory - checkpoint_dir = os.path.join(cfg.output_dir, CHECKPOINTS_DIR, LAST_CHECKPOINT_LINK) + checkpoint_dir = Path(cfg.output_dir) / CHECKPOINTS_DIR / LAST_CHECKPOINT_LINK logging.info(f"Loading training state from {checkpoint_dir}") try: - # Use the utility function from train_utils which loads the optimizer state - step, optimizers, _ = utils_load_training_state(Path(checkpoint_dir), optimizers, None) + # Restore optimizers + RNG + step from the standard `training_state/` folder + step, optimizers, _ = utils_load_training_state(checkpoint_dir, optimizers, None) - # Load interaction step separately from training_state.pt - training_state_path = os.path.join(checkpoint_dir, TRAINING_STATE_DIR, "training_state.pt") - interaction_step = 0 - if os.path.exists(training_state_path): - training_state = torch.load(training_state_path, weights_only=False) # nosec B614: Safe usage of torch.load - interaction_step = training_state.get("interaction_step", 0) + # Restore algorithm-owned tensors + if algorithm is not None: + algo_dir = checkpoint_dir / ALGORITHM_DIR + if algo_dir.is_dir(): + tensors = load_safetensors(str(algo_dir / SAFETENSORS_SINGLE_FILE)) + algorithm.load_state_dict(tensors, device=device) + logging.info(f"Loaded algorithm state from {algo_dir}") + else: + logging.warning( + f"No algorithm state found at {algo_dir}; " + "will keep their freshly-initialised values. Adam moments restored from the " + "old optimizer state may not match these reset parameters." + ) + + # Read interaction_step from the enriched training_step.json + training_step_path = checkpoint_dir / TRAINING_STATE_DIR / TRAINING_STEP + interaction_step = int(load_json(training_step_path).get("interaction_step", 0)) logging.info(f"Resuming from step {step}, interaction step {interaction_step}") return step, interaction_step @@ -1016,33 +869,6 @@ def initialize_offline_replay_buffer( # Utilities/Helpers functions -def get_observation_features( - policy: SACPolicy, observations: torch.Tensor, next_observations: torch.Tensor -) -> tuple[torch.Tensor | None, torch.Tensor | None]: - """ - Get observation features from the policy encoder. It act as cache for the observation features. - when the encoder is frozen, the observation features are not updated. - We can save compute by caching the observation features. - - Args: - policy: The policy model - observations: The current observations - next_observations: The next observations - - Returns: - tuple: observation_features, next_observation_features - """ - - if policy.config.vision_encoder_name is None or not policy.config.freeze_vision_encoder: - return None, None - - with torch.no_grad(): - observation_features = policy.actor.encoder.get_cached_image_features(observations) - next_observation_features = policy.actor.encoder.get_cached_image_features(next_observations) - - return observation_features, next_observation_features - - def use_threads(cfg: TrainRLServerPipelineConfig) -> bool: return cfg.policy.concurrency.learner == "threads" @@ -1093,19 +919,11 @@ def check_nan_in_transition( return nan_detected -def push_actor_policy_to_queue(parameters_queue: Queue, policy: nn.Module): +def push_actor_policy_to_queue(parameters_queue: Queue, algorithm: RLAlgorithm) -> None: logging.debug("[LEARNER] Pushing actor policy to the queue") # Create a dictionary to hold all the state dicts - state_dicts = {"policy": move_state_dict_to_device(policy.actor.state_dict(), device="cpu")} - - # Add discrete critic if it exists - if hasattr(policy, "discrete_critic") and policy.discrete_critic is not None: - state_dicts["discrete_critic"] = move_state_dict_to_device( - policy.discrete_critic.state_dict(), device="cpu" - ) - logging.debug("[LEARNER] Including discrete critic in state dict push") - + state_dicts = algorithm.get_weights() state_bytes = state_to_bytes(state_dicts) parameters_queue.put(state_bytes) @@ -1129,9 +947,8 @@ def process_transitions( transition_queue: Queue, replay_buffer: ReplayBuffer, offline_replay_buffer: ReplayBuffer, - device: str, dataset_repo_id: str | None, - shutdown_event: any, + shutdown_event: Any, # Event ): """Process all available transitions from the queue. @@ -1139,7 +956,6 @@ def process_transitions( transition_queue: Queue for receiving transitions from the actor replay_buffer: Replay buffer to add transitions to offline_replay_buffer: Offline replay buffer to add transitions to - device: Device to move transitions to dataset_repo_id: Repository ID for dataset shutdown_event: Event to signal shutdown """ @@ -1148,8 +964,6 @@ def process_transitions( transition_list = bytes_to_transitions(buffer=transition_list) for transition in transition_list: - transition = move_transition_to_device(transition=transition, device=device) - # Skip transitions with NaN values if check_nan_in_transition( observations=transition["state"], @@ -1163,7 +977,7 @@ def process_transitions( # Add to offline buffer if it's an intervention if dataset_repo_id is not None and transition.get("complementary_info", {}).get( - TeleopEvents.IS_INTERVENTION + TeleopEvents.IS_INTERVENTION.value ): offline_replay_buffer.add(**transition) @@ -1172,7 +986,7 @@ def process_interaction_messages( interaction_message_queue: Queue, interaction_step_shift: int, wandb_logger: WandBLogger | None, - shutdown_event: any, + shutdown_event: Any, # Event ) -> dict | None: """Process all available interaction messages from the queue. diff --git a/src/lerobot/rl/learner_service.py b/src/lerobot/rl/learner_service.py index 4128cdf55..7a4df7136 100644 --- a/src/lerobot/rl/learner_service.py +++ b/src/lerobot/rl/learner_service.py @@ -18,17 +18,32 @@ import logging import time from multiprocessing import Event, Queue +from typing import TYPE_CHECKING -from lerobot.transport import services_pb2, services_pb2_grpc -from lerobot.transport.utils import receive_bytes_in_chunks, send_bytes_in_chunks +from lerobot.utils.import_utils import _grpc_available from .queue import get_last_item_from_queue +if TYPE_CHECKING or _grpc_available: + import grpc + + from lerobot.transport import services_pb2, services_pb2_grpc + from lerobot.transport.utils import receive_bytes_in_chunks, send_bytes_in_chunks + + _ServicerBase = services_pb2_grpc.LearnerServiceServicer +else: + grpc = None + services_pb2 = None + services_pb2_grpc = None + receive_bytes_in_chunks = None + send_bytes_in_chunks = None + _ServicerBase = object + MAX_WORKERS = 3 # Stream parameters, send transitions and interactions SHUTDOWN_TIMEOUT = 10 -class LearnerService(services_pb2_grpc.LearnerServiceServicer): +class LearnerService(_ServicerBase): """ Implementation of the LearnerService gRPC service This service is used to send parameters to the Actor and receive transitions and interactions from the Actor @@ -51,7 +66,9 @@ class LearnerService(services_pb2_grpc.LearnerServiceServicer): self.interaction_message_queue = interaction_message_queue self.queue_get_timeout = queue_get_timeout - def StreamParameters(self, request, context): # noqa: N802 + def StreamParameters( # noqa: N802 + self, request: "services_pb2.Empty", context: "grpc.ServicerContext" + ): # TODO: authorize the request logging.info("[LEARNER] Received request to stream parameters from the Actor") @@ -86,7 +103,7 @@ class LearnerService(services_pb2_grpc.LearnerServiceServicer): logging.info("[LEARNER] Stream parameters finished") return services_pb2.Empty() - def SendTransitions(self, request_iterator, _context): # noqa: N802 + def SendTransitions(self, request_iterator, _context: "grpc.ServicerContext"): # noqa: N802 # TODO: authorize the request logging.info("[LEARNER] Received request to receive transitions from the Actor") @@ -100,7 +117,7 @@ class LearnerService(services_pb2_grpc.LearnerServiceServicer): logging.debug("[LEARNER] Finished receiving transitions") return services_pb2.Empty() - def SendInteractions(self, request_iterator, _context): # noqa: N802 + def SendInteractions(self, request_iterator, _context: "grpc.ServicerContext"): # noqa: N802 # TODO: authorize the request logging.info("[LEARNER] Received request to receive interactions from the Actor") @@ -114,5 +131,5 @@ class LearnerService(services_pb2_grpc.LearnerServiceServicer): logging.debug("[LEARNER] Finished receiving interactions") return services_pb2.Empty() - def Ready(self, request, context): # noqa: N802 + def Ready(self, request: "services_pb2.Empty", context: "grpc.ServicerContext"): # noqa: N802 return services_pb2.Empty() diff --git a/src/lerobot/rl/train_rl.py b/src/lerobot/rl/train_rl.py new file mode 100644 index 000000000..e5ae0f9f5 --- /dev/null +++ b/src/lerobot/rl/train_rl.py @@ -0,0 +1,50 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Top-level pipeline config for distributed RL training (actor / learner).""" + +from __future__ import annotations + +from dataclasses import dataclass + +from lerobot.configs.default import DatasetConfig +from lerobot.configs.train import TrainPipelineConfig + +from .algorithms.configs import RLAlgorithmConfig +from .algorithms.factory import make_algorithm_config +from .algorithms.sac import SACAlgorithmConfig # noqa: F401 + + +@dataclass(kw_only=True) +class TrainRLServerPipelineConfig(TrainPipelineConfig): + # NOTE: In RL, we don't need an offline dataset + # TODO: Make `TrainPipelineConfig.dataset` optional + dataset: DatasetConfig | None = None # type: ignore[assignment] # because the parent class has made it's type non-optional + + # Algorithm config. + algorithm: RLAlgorithmConfig | None = None + + # Data mixer strategy name. Currently supports "online_offline". + mixer: str = "online_offline" + # Fraction sampled from online replay when using OnlineOfflineMixer. + online_ratio: float = 0.5 + + def validate(self) -> None: + super().validate() + + if self.algorithm is None: + self.algorithm = make_algorithm_config("sac") + + if getattr(self.algorithm, "policy_config", None) is None: + self.algorithm.policy_config = self.policy diff --git a/src/lerobot/rl/trainer.py b/src/lerobot/rl/trainer.py new file mode 100644 index 000000000..65f00568e --- /dev/null +++ b/src/lerobot/rl/trainer.py @@ -0,0 +1,101 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from collections.abc import Iterator +from typing import Any + +from lerobot.types import BatchType + +from .algorithms.base import RLAlgorithm +from .algorithms.configs import TrainingStats +from .data_sources.data_mixer import DataMixer + + +class RLTrainer: + """Unified training step orchestrator. + + Holds the algorithm, a DataMixer, and an optional preprocessor. + """ + + def __init__( + self, + algorithm: RLAlgorithm, + data_mixer: DataMixer, + batch_size: int, + *, + preprocessor: Any | None = None, + ): + self.algorithm = algorithm + self.data_mixer = data_mixer + self.batch_size = batch_size + self._preprocessor = preprocessor + + self._iterator: Iterator[BatchType] | None = None + + self.algorithm.make_optimizers_and_scheduler() + + def _build_data_iterator(self) -> Iterator[BatchType]: + """Create a fresh algorithm-configured iterator (optionally preprocessed).""" + raw = self.algorithm.configure_data_iterator( + data_mixer=self.data_mixer, + batch_size=self.batch_size, + ) + if self._preprocessor is not None: + return _PreprocessedIterator(raw, self._preprocessor) + return raw + + def reset_data_iterator(self) -> None: + """Discard the current iterator so it will be rebuilt lazily next step.""" + self._iterator = None + + def set_data_mixer(self, data_mixer: DataMixer, *, reset: bool = True) -> None: + """Swap the active data mixer, optionally resetting the iterator.""" + self.data_mixer = data_mixer + if reset: + self.reset_data_iterator() + + def training_step(self) -> TrainingStats: + """Run one training step (algorithm-agnostic).""" + if self._iterator is None: + self._iterator = self._build_data_iterator() + return self.algorithm.update(self._iterator) + + +def preprocess_rl_batch(preprocessor: Any, batch: BatchType) -> BatchType: + """Apply policy preprocessing to RL observations only.""" + observations = batch["state"] + next_observations = batch["next_state"] + batch["state"] = preprocessor.process_observation(observations) + batch["next_state"] = preprocessor.process_observation(next_observations) + + return batch + + +class _PreprocessedIterator: + """Iterator wrapper that preprocesses each sampled RL batch.""" + + __slots__ = ("_raw", "_preprocessor") + + def __init__(self, raw_iterator: Iterator[BatchType], preprocessor: Any) -> None: + self._raw = raw_iterator + self._preprocessor = preprocessor + + def __iter__(self) -> _PreprocessedIterator: + return self + + def __next__(self) -> BatchType: + batch = next(self._raw) + return preprocess_rl_batch(self._preprocessor, batch) diff --git a/src/lerobot/robots/so_follower/robot_kinematic_processor.py b/src/lerobot/robots/so_follower/robot_kinematic_processor.py index 8114fdc2c..a95343b2d 100644 --- a/src/lerobot/robots/so_follower/robot_kinematic_processor.py +++ b/src/lerobot/robots/so_follower/robot_kinematic_processor.py @@ -353,7 +353,8 @@ class GripperVelocityToJoint(RobotActionProcessorStep): speed_factor: A scaling factor to convert the normalized velocity command to a position change. clip_min: The minimum allowed gripper joint position. clip_max: The maximum allowed gripper joint position. - discrete_gripper: If True, treat the input action as discrete (0: open, 1: close, 2: stay). + discrete_gripper: If True, interpret the input as a discrete class index + {0 = close, 1 = stay, 2 = open}, matching `GamepadTeleop.GripperAction`. """ speed_factor: float = 20.0 @@ -377,10 +378,10 @@ class GripperVelocityToJoint(RobotActionProcessorStep): raise ValueError("Joints observation is require for computing robot kinematics") if self.discrete_gripper: - # Discrete gripper actions are in [0, 1, 2] - # 0: open, 1: close, 2: stay - # We need to shift them to [-1, 0, 1] and then scale them to clip_max - gripper_vel = (gripper_vel - 1) * self.clip_max + # Map discrete command {0=close, 1=stay, 2=open} -> signed velocity. + # Negation accounts for SO100 sign (joint position increases on close). + # 0 -> +clip_max (close), 1 -> 0 (stay), 2 -> -clip_max (open) + gripper_vel = -(gripper_vel - 1) * self.clip_max # Compute desired gripper position delta = gripper_vel * float(self.speed_factor) diff --git a/src/lerobot/teleoperators/keyboard/teleop_keyboard.py b/src/lerobot/teleoperators/keyboard/teleop_keyboard.py index 6fc553d38..801789bcb 100644 --- a/src/lerobot/teleoperators/keyboard/teleop_keyboard.py +++ b/src/lerobot/teleoperators/keyboard/teleop_keyboard.py @@ -104,11 +104,14 @@ class KeyboardTeleop(Teleoperator): def _on_press(self, key): if hasattr(key, "char"): - self.event_queue.put((key.char, True)) + key = key.char + self.event_queue.put((key, True)) def _on_release(self, key): if hasattr(key, "char"): - self.event_queue.put((key.char, False)) + key = key.char + self.event_queue.put((key, False)) + if key == keyboard.Key.esc: logging.info("ESC pressed, disconnecting.") self.disconnect() @@ -204,8 +207,6 @@ class KeyboardEndEffectorTeleop(KeyboardTeleop): # this is useful for retrieving other events like interventions for RL, episode success, etc. self.misc_keys_queue.put(key) - self.current_pressed.clear() - action_dict = { "delta_x": delta_x, "delta_y": delta_y, @@ -256,6 +257,8 @@ class KeyboardEndEffectorTeleop(KeyboardTeleop): ] is_intervention = any(self.current_pressed.get(key, False) for key in movement_keys) + self.current_pressed.clear() + # Check for episode control commands from misc_keys_queue terminate_episode = False success = False diff --git a/src/lerobot/templates/lerobot_modelcard_template.md b/src/lerobot/templates/lerobot_modelcard_template.md index c59cf4183..f0dd0da07 100644 --- a/src/lerobot/templates/lerobot_modelcard_template.md +++ b/src/lerobot/templates/lerobot_modelcard_template.md @@ -39,8 +39,8 @@ For more details, see the [Physical Intelligence π₀ blog post](https://www.ph π₀.₅ represents a significant evolution from π₀, developed by Physical Intelligence to address a big challenge in robotics: open-world generalization. While robots can perform impressive tasks in controlled environments, π₀.₅ is designed to generalize to entirely new environments and situations that were never seen during training. For more details, see the [Physical Intelligence π₀.₅ blog post](https://www.physicalintelligence.company/blog/pi05). -{% elif model_name == "sac" %} -[Soft Actor-Critic (SAC)](https://huggingface.co/papers/1801.01290) is an entropy-regularised actor-critic algorithm offering stable, sample-efficient learning in continuous-control environments. +{% elif model_name == "gaussian_actor" %} +This is a Gaussian Actor policy (Gaussian policy with a tanh squash) — the policy-side component used by [Soft Actor-Critic (SAC)](https://huggingface.co/papers/1801.01290) and related maximum-entropy continuous-control algorithms. {% elif model_name == "reward_classifier" %} A reward classifier is a lightweight neural network that scores observations or trajectories for task success, providing a learned reward signal or offline evaluation when explicit rewards are unavailable. {% else %} diff --git a/src/lerobot/types.py b/src/lerobot/types.py index d9b8166c5..9de504870 100644 --- a/src/lerobot/types.py +++ b/src/lerobot/types.py @@ -40,6 +40,7 @@ PolicyAction = torch.Tensor RobotAction = dict[str, Any] EnvAction = np.ndarray RobotObservation = dict[str, Any] +BatchType = dict[str, Any] EnvTransition = TypedDict( diff --git a/src/lerobot/utils/constants.py b/src/lerobot/utils/constants.py index 43869228d..482394ff6 100644 --- a/src/lerobot/utils/constants.py +++ b/src/lerobot/utils/constants.py @@ -47,6 +47,7 @@ CHECKPOINTS_DIR = "checkpoints" LAST_CHECKPOINT_LINK = "last" PRETRAINED_MODEL_DIR = "pretrained_model" TRAINING_STATE_DIR = "training_state" +ALGORITHM_DIR = "algorithm" RNG_STATE = "rng_state.safetensors" TRAINING_STEP = "training_step.json" OPTIMIZER_STATE = "optimizer_state.safetensors" diff --git a/src/lerobot/utils/import_utils.py b/src/lerobot/utils/import_utils.py index bfa87fb86..6ba912bf5 100644 --- a/src/lerobot/utils/import_utils.py +++ b/src/lerobot/utils/import_utils.py @@ -132,6 +132,7 @@ _faker_available = is_package_available("faker") _pynput_available = is_package_available("pynput") _pygame_available = is_package_available("pygame") _qwen_vl_utils_available = is_package_available("qwen-vl-utils", import_name="qwen_vl_utils") +_grpc_available = is_package_available("grpcio", import_name="grpc") _wallx_deps_available = ( _transformers_available and _peft_available and _torchdiffeq_available and _qwen_vl_utils_available ) diff --git a/tests/policies/test_sac_config.py b/tests/policies/test_gaussian_actor_config.py similarity index 81% rename from tests/policies/test_sac_config.py rename to tests/policies/test_gaussian_actor_config.py index 724c331ff..004612374 100644 --- a/tests/policies/test_sac_config.py +++ b/tests/policies/test_gaussian_actor_config.py @@ -17,19 +17,19 @@ import pytest from lerobot.configs.types import FeatureType, NormalizationMode, PolicyFeature -from lerobot.policies.sac.configuration_sac import ( +from lerobot.policies.gaussian_actor.configuration_gaussian_actor import ( ActorLearnerConfig, ActorNetworkConfig, ConcurrencyConfig, CriticNetworkConfig, + GaussianActorConfig, PolicyConfig, - SACConfig, ) from lerobot.utils.constants import ACTION, OBS_IMAGE, OBS_STATE -def test_sac_config_default_initialization(): - config = SACConfig() +def test_gaussian_actor_config_default_initialization(): + config = GaussianActorConfig() assert config.normalization_mapping == { "VISUAL": NormalizationMode.MEAN_STD, @@ -55,9 +55,6 @@ def test_sac_config_default_initialization(): # Basic parameters assert config.device == "cpu" assert config.storage_device == "cpu" - assert config.discount == 0.99 - assert config.temperature_init == 1.0 - assert config.num_critics == 2 # Architecture specifics assert config.vision_encoder_name is None @@ -66,6 +63,8 @@ def test_sac_config_default_initialization(): assert config.shared_encoder is True assert config.num_discrete_actions is None assert config.image_embedding_pooling_dim == 8 + assert config.state_encoder_hidden_dim == 256 + assert config.latent_dim == 256 # Training parameters assert config.online_steps == 1000000 @@ -73,20 +72,6 @@ def test_sac_config_default_initialization(): assert config.offline_buffer_capacity == 100000 assert config.async_prefetch is False assert config.online_step_before_learning == 100 - assert config.policy_update_freq == 1 - - # SAC algorithm parameters - assert config.num_subsample_critics is None - assert config.critic_lr == 3e-4 - assert config.actor_lr == 3e-4 - assert config.temperature_lr == 3e-4 - assert config.critic_target_update_weight == 0.005 - assert config.utd_ratio == 1 - assert config.state_encoder_hidden_dim == 256 - assert config.latent_dim == 256 - assert config.target_entropy is None - assert config.use_backup_entropy is True - assert config.grad_clip_norm == 40.0 # Dataset stats defaults expected_dataset_stats = { @@ -105,11 +90,6 @@ def test_sac_config_default_initialization(): } assert config.dataset_stats == expected_dataset_stats - # Critic network configuration - assert config.critic_network_kwargs.hidden_dims == [256, 256] - assert config.critic_network_kwargs.activate_final is True - assert config.critic_network_kwargs.final_activation is None - # Actor network configuration assert config.actor_network_kwargs.hidden_dims == [256, 256] assert config.actor_network_kwargs.activate_final is True @@ -135,7 +115,6 @@ def test_sac_config_default_initialization(): assert config.concurrency.learner == "threads" assert isinstance(config.actor_network_kwargs, ActorNetworkConfig) - assert isinstance(config.critic_network_kwargs, CriticNetworkConfig) assert isinstance(config.policy_kwargs, PolicyConfig) assert isinstance(config.actor_learner_config, ActorLearnerConfig) assert isinstance(config.concurrency, ConcurrencyConfig) @@ -175,22 +154,22 @@ def test_concurrency_config(): assert config.learner == "threads" -def test_sac_config_custom_initialization(): - config = SACConfig( +def test_gaussian_actor_config_custom_initialization(): + config = GaussianActorConfig( device="cpu", - discount=0.95, - temperature_init=0.5, - num_critics=3, + latent_dim=128, + state_encoder_hidden_dim=128, + num_discrete_actions=3, ) assert config.device == "cpu" - assert config.discount == 0.95 - assert config.temperature_init == 0.5 - assert config.num_critics == 3 + assert config.latent_dim == 128 + assert config.state_encoder_hidden_dim == 128 + assert config.num_discrete_actions == 3 def test_validate_features(): - config = SACConfig( + config = GaussianActorConfig( input_features={OBS_STATE: PolicyFeature(type=FeatureType.STATE, shape=(10,))}, output_features={ACTION: PolicyFeature(type=FeatureType.ACTION, shape=(3,))}, ) @@ -198,7 +177,7 @@ def test_validate_features(): def test_validate_features_missing_observation(): - config = SACConfig( + config = GaussianActorConfig( input_features={"wrong_key": PolicyFeature(type=FeatureType.STATE, shape=(10,))}, output_features={ACTION: PolicyFeature(type=FeatureType.ACTION, shape=(3,))}, ) @@ -209,7 +188,7 @@ def test_validate_features_missing_observation(): def test_validate_features_missing_action(): - config = SACConfig( + config = GaussianActorConfig( input_features={OBS_STATE: PolicyFeature(type=FeatureType.STATE, shape=(10,))}, output_features={"wrong_key": PolicyFeature(type=FeatureType.ACTION, shape=(3,))}, ) diff --git a/tests/policies/test_gaussian_actor_policy.py b/tests/policies/test_gaussian_actor_policy.py new file mode 100644 index 000000000..af802d26f --- /dev/null +++ b/tests/policies/test_gaussian_actor_policy.py @@ -0,0 +1,528 @@ +# !/usr/bin/env python + +# Copyright 2025 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])") + +import torch # noqa: E402 +from torch import Tensor, nn # noqa: E402 + +from lerobot.configs.types import FeatureType, PolicyFeature # noqa: E402 +from lerobot.policies.gaussian_actor.configuration_gaussian_actor import GaussianActorConfig # noqa: E402 +from lerobot.policies.gaussian_actor.modeling_gaussian_actor import MLP, GaussianActorPolicy # noqa: E402 +from lerobot.rl.algorithms.sac import SACAlgorithm, SACAlgorithmConfig # noqa: E402 +from lerobot.utils.constants import ACTION, OBS_IMAGE, OBS_STATE # noqa: E402 +from lerobot.utils.random_utils import seeded_context, set_seed # noqa: E402 + +try: + import transformers # noqa: F401 + + TRANSFORMERS_AVAILABLE = True +except ImportError: + TRANSFORMERS_AVAILABLE = False + + +@pytest.fixture(autouse=True) +def set_random_seed(): + seed = 42 + set_seed(seed) + + +def test_mlp_with_default_args(): + mlp = MLP(input_dim=10, hidden_dims=[256, 256]) + + x = torch.randn(10) + y = mlp(x) + assert y.shape == (256,) + + +def test_mlp_with_batch_dim(): + mlp = MLP(input_dim=10, hidden_dims=[256, 256]) + x = torch.randn(2, 10) + y = mlp(x) + assert y.shape == (2, 256) + + +def test_forward_with_empty_hidden_dims(): + mlp = MLP(input_dim=10, hidden_dims=[]) + x = torch.randn(1, 10) + assert mlp(x).shape == (1, 10) + + +def test_mlp_with_dropout(): + mlp = MLP(input_dim=10, hidden_dims=[256, 256, 11], dropout_rate=0.1) + x = torch.randn(1, 10) + y = mlp(x) + assert y.shape == (1, 11) + + drop_out_layers_count = sum(isinstance(layer, nn.Dropout) for layer in mlp.net) + assert drop_out_layers_count == 2 + + +def test_mlp_with_custom_final_activation(): + mlp = MLP(input_dim=10, hidden_dims=[256, 256], final_activation=torch.nn.Tanh()) + x = torch.randn(1, 10) + y = mlp(x) + assert y.shape == (1, 256) + assert (y >= -1).all() and (y <= 1).all() + + +def test_gaussian_actor_policy_with_default_args(): + with pytest.raises(ValueError, match="should be an instance of class `PreTrainedConfig`"): + GaussianActorPolicy() + + +def create_dummy_state(batch_size: int, state_dim: int = 10) -> Tensor: + return { + OBS_STATE: torch.randn(batch_size, state_dim), + } + + +def create_dummy_with_visual_input(batch_size: int, state_dim: int = 10) -> Tensor: + return { + OBS_IMAGE: torch.randn(batch_size, 3, 84, 84), + OBS_STATE: torch.randn(batch_size, state_dim), + } + + +def create_dummy_action(batch_size: int, action_dim: int = 10) -> Tensor: + return torch.randn(batch_size, action_dim) + + +def create_default_train_batch( + batch_size: int = 8, state_dim: int = 10, action_dim: int = 10 +) -> dict[str, Tensor]: + return { + ACTION: create_dummy_action(batch_size, action_dim), + "reward": torch.randn(batch_size), + "state": create_dummy_state(batch_size, state_dim), + "next_state": create_dummy_state(batch_size, state_dim), + "done": torch.randn(batch_size), + } + + +def create_train_batch_with_visual_input( + batch_size: int = 8, state_dim: int = 10, action_dim: int = 10 +) -> dict[str, Tensor]: + return { + ACTION: create_dummy_action(batch_size, action_dim), + "reward": torch.randn(batch_size), + "state": create_dummy_with_visual_input(batch_size, state_dim), + "next_state": create_dummy_with_visual_input(batch_size, state_dim), + "done": torch.randn(batch_size), + } + + +def create_observation_batch(batch_size: int = 8, state_dim: int = 10) -> dict[str, Tensor]: + return { + OBS_STATE: torch.randn(batch_size, state_dim), + } + + +def create_observation_batch_with_visual_input(batch_size: int = 8, state_dim: int = 10) -> dict[str, Tensor]: + return { + OBS_STATE: torch.randn(batch_size, state_dim), + OBS_IMAGE: torch.randn(batch_size, 3, 84, 84), + } + + +def create_default_config( + state_dim: int, continuous_action_dim: int, has_discrete_action: bool = False +) -> GaussianActorConfig: + action_dim = continuous_action_dim + if has_discrete_action: + action_dim += 1 + + config = GaussianActorConfig( + input_features={OBS_STATE: PolicyFeature(type=FeatureType.STATE, shape=(state_dim,))}, + output_features={ACTION: PolicyFeature(type=FeatureType.ACTION, shape=(continuous_action_dim,))}, + dataset_stats={ + OBS_STATE: { + "min": [0.0] * state_dim, + "max": [1.0] * state_dim, + }, + ACTION: { + "min": [0.0] * continuous_action_dim, + "max": [1.0] * continuous_action_dim, + }, + }, + ) + config.validate_features() + return config + + +def create_config_with_visual_input( + state_dim: int, continuous_action_dim: int, has_discrete_action: bool = False +) -> GaussianActorConfig: + config = create_default_config( + state_dim=state_dim, + continuous_action_dim=continuous_action_dim, + has_discrete_action=has_discrete_action, + ) + config.input_features[OBS_IMAGE] = PolicyFeature(type=FeatureType.VISUAL, shape=(3, 84, 84)) + config.dataset_stats[OBS_IMAGE] = { + "mean": torch.randn(3, 1, 1), + "std": torch.randn(3, 1, 1), + } + + config.state_encoder_hidden_dim = 32 + config.latent_dim = 32 + + config.validate_features() + return config + + +def _make_algorithm(config: GaussianActorConfig) -> tuple[SACAlgorithm, GaussianActorPolicy]: + """Helper to create policy + algorithm pair for tests that need critics.""" + policy = GaussianActorPolicy(config=config) + policy.train() + algo_config = SACAlgorithmConfig.from_policy_config(config) + algorithm = SACAlgorithm(policy=policy, config=algo_config) + algorithm.make_optimizers_and_scheduler() + return algorithm, policy + + +@pytest.mark.parametrize("batch_size,state_dim,action_dim", [(2, 6, 6), (1, 10, 10)]) +def test_gaussian_actor_policy_select_action(batch_size: int, state_dim: int, action_dim: int): + config = create_default_config(state_dim=state_dim, continuous_action_dim=action_dim) + policy = GaussianActorPolicy(config=config) + policy.eval() + + with torch.no_grad(): + observation_batch = create_observation_batch(batch_size=batch_size, state_dim=state_dim) + selected_action = policy.select_action(observation_batch) + # squeeze(0) removes batch dim when batch_size==1 + assert selected_action.shape[-1] == action_dim + + +def test_gaussian_actor_policy_select_action_with_discrete(): + """select_action should return continuous + discrete actions.""" + config = create_default_config(state_dim=10, continuous_action_dim=6) + config.num_discrete_actions = 3 + policy = GaussianActorPolicy(config=config) + policy.eval() + + with torch.no_grad(): + observation_batch = create_observation_batch(batch_size=1, state_dim=10) + # Squeeze to unbatched (single observation) + observation_batch = {k: v.squeeze(0) for k, v in observation_batch.items()} + selected_action = policy.select_action(observation_batch) + assert selected_action.shape[-1] == 7 # 6 continuous + 1 discrete + + +@pytest.mark.parametrize("batch_size,state_dim,action_dim", [(2, 6, 6), (1, 10, 10)]) +def test_gaussian_actor_policy_forward(batch_size: int, state_dim: int, action_dim: int): + config = create_default_config(state_dim=state_dim, continuous_action_dim=action_dim) + policy = GaussianActorPolicy(config=config) + policy.eval() + + batch = create_default_train_batch(batch_size=batch_size, action_dim=action_dim, state_dim=state_dim) + with torch.no_grad(): + output = policy.forward(batch) + assert "action" in output + assert "log_prob" in output + assert "action_mean" in output + assert output["action"].shape == (batch_size, action_dim) + + +@pytest.mark.parametrize("batch_size,state_dim,action_dim", [(2, 6, 6), (1, 10, 10)]) +def test_gaussian_actor_training_through_sac(batch_size: int, state_dim: int, action_dim: int): + config = create_default_config(state_dim=state_dim, continuous_action_dim=action_dim) + algorithm, policy = _make_algorithm(config) + + batch = create_default_train_batch(batch_size=batch_size, action_dim=action_dim, state_dim=state_dim) + forward_batch = algorithm._prepare_forward_batch(batch) + + critic_loss = algorithm._compute_loss_critic(forward_batch) + assert critic_loss.item() is not None + assert critic_loss.shape == () + algorithm.optimizers["critic"].zero_grad() + critic_loss.backward() + algorithm.optimizers["critic"].step() + + actor_loss = algorithm._compute_loss_actor(forward_batch) + assert actor_loss.item() is not None + assert actor_loss.shape == () + algorithm.optimizers["actor"].zero_grad() + actor_loss.backward() + algorithm.optimizers["actor"].step() + + temp_loss = algorithm._compute_loss_temperature(forward_batch) + assert temp_loss.item() is not None + assert temp_loss.shape == () + algorithm.optimizers["temperature"].zero_grad() + temp_loss.backward() + algorithm.optimizers["temperature"].step() + + +@pytest.mark.parametrize("batch_size,state_dim,action_dim", [(2, 6, 6), (1, 10, 10)]) +def test_gaussian_actor_training_with_visual_input(batch_size: int, state_dim: int, action_dim: int): + config = create_config_with_visual_input(state_dim=state_dim, continuous_action_dim=action_dim) + algorithm, policy = _make_algorithm(config) + + batch = create_train_batch_with_visual_input( + batch_size=batch_size, state_dim=state_dim, action_dim=action_dim + ) + forward_batch = algorithm._prepare_forward_batch(batch) + + critic_loss = algorithm._compute_loss_critic(forward_batch) + assert critic_loss.item() is not None + assert critic_loss.shape == () + algorithm.optimizers["critic"].zero_grad() + critic_loss.backward() + algorithm.optimizers["critic"].step() + + actor_loss = algorithm._compute_loss_actor(forward_batch) + assert actor_loss.item() is not None + assert actor_loss.shape == () + algorithm.optimizers["actor"].zero_grad() + actor_loss.backward() + algorithm.optimizers["actor"].step() + + policy.eval() + with torch.no_grad(): + observation_batch = create_observation_batch_with_visual_input( + batch_size=batch_size, state_dim=state_dim + ) + selected_action = policy.select_action(observation_batch) + assert selected_action.shape[-1] == action_dim + + +@pytest.mark.parametrize( + "batch_size,state_dim,action_dim,vision_encoder_name", + [(1, 6, 6, "lerobot/resnet10"), (1, 6, 6, "facebook/convnext-base-224")], +) +@pytest.mark.skipif(not TRANSFORMERS_AVAILABLE, reason="Transformers are not installed") +def test_gaussian_actor_policy_with_pretrained_encoder( + batch_size: int, state_dim: int, action_dim: int, vision_encoder_name: str +): + config = create_config_with_visual_input(state_dim=state_dim, continuous_action_dim=action_dim) + config.vision_encoder_name = vision_encoder_name + algorithm, policy = _make_algorithm(config) + + batch = create_train_batch_with_visual_input( + batch_size=batch_size, state_dim=state_dim, action_dim=action_dim + ) + forward_batch = algorithm._prepare_forward_batch(batch) + + critic_loss = algorithm._compute_loss_critic(forward_batch) + assert critic_loss.item() is not None + assert critic_loss.shape == () + algorithm.optimizers["critic"].zero_grad() + critic_loss.backward() + algorithm.optimizers["critic"].step() + + actor_loss = algorithm._compute_loss_actor(forward_batch) + assert actor_loss.item() is not None + assert actor_loss.shape == () + + +def test_gaussian_actor_training_with_shared_encoder(): + batch_size = 2 + action_dim = 10 + state_dim = 10 + config = create_config_with_visual_input(state_dim=state_dim, continuous_action_dim=action_dim) + config.shared_encoder = True + + algorithm, policy = _make_algorithm(config) + + batch = create_train_batch_with_visual_input( + batch_size=batch_size, state_dim=state_dim, action_dim=action_dim + ) + forward_batch = algorithm._prepare_forward_batch(batch) + + critic_loss = algorithm._compute_loss_critic(forward_batch) + assert critic_loss.shape == () + algorithm.optimizers["critic"].zero_grad() + critic_loss.backward() + algorithm.optimizers["critic"].step() + + actor_loss = algorithm._compute_loss_actor(forward_batch) + assert actor_loss.shape == () + algorithm.optimizers["actor"].zero_grad() + actor_loss.backward() + algorithm.optimizers["actor"].step() + + +def test_gaussian_actor_training_with_discrete_critic(): + batch_size = 2 + continuous_action_dim = 9 + full_action_dim = continuous_action_dim + 1 + state_dim = 10 + config = create_config_with_visual_input( + state_dim=state_dim, continuous_action_dim=continuous_action_dim, has_discrete_action=True + ) + config.num_discrete_actions = 5 + + algorithm, policy = _make_algorithm(config) + + batch = create_train_batch_with_visual_input( + batch_size=batch_size, state_dim=state_dim, action_dim=full_action_dim + ) + forward_batch = algorithm._prepare_forward_batch(batch) + + critic_loss = algorithm._compute_loss_critic(forward_batch) + assert critic_loss.shape == () + algorithm.optimizers["critic"].zero_grad() + critic_loss.backward() + algorithm.optimizers["critic"].step() + + discrete_critic_loss = algorithm._compute_loss_discrete_critic(forward_batch) + assert discrete_critic_loss.shape == () + algorithm.optimizers["discrete_critic"].zero_grad() + discrete_critic_loss.backward() + algorithm.optimizers["discrete_critic"].step() + + actor_loss = algorithm._compute_loss_actor(forward_batch) + assert actor_loss.shape == () + algorithm.optimizers["actor"].zero_grad() + actor_loss.backward() + algorithm.optimizers["actor"].step() + + policy.eval() + with torch.no_grad(): + observation_batch = create_observation_batch_with_visual_input( + batch_size=batch_size, state_dim=state_dim + ) + # Policy.select_action now handles both continuous + discrete + selected_action = policy.select_action({k: v.squeeze(0) for k, v in observation_batch.items()}) + assert selected_action.shape[-1] == continuous_action_dim + 1 + + +def test_sac_algorithm_target_entropy(): + """Target entropy is an SAC hyperparameter and lives on the algorithm.""" + config = create_default_config(continuous_action_dim=10, state_dim=10) + algorithm, _ = _make_algorithm(config) + assert algorithm.target_entropy == -5.0 + + +def test_sac_algorithm_target_entropy_with_discrete_action(): + config = create_config_with_visual_input(state_dim=10, continuous_action_dim=6, has_discrete_action=True) + config.num_discrete_actions = 5 + algorithm, _ = _make_algorithm(config) + assert algorithm.target_entropy == -3.5 + + +def test_sac_algorithm_temperature(): + import math + + config = create_default_config(continuous_action_dim=10, state_dim=10) + algo_config = SACAlgorithmConfig.from_policy_config(config) + policy = GaussianActorPolicy(config=config) + algorithm = SACAlgorithm(policy=policy, config=algo_config) + + assert algorithm.temperature == pytest.approx(1.0) + algorithm.log_alpha.data = torch.tensor([math.log(0.1)]) + assert algorithm.temperature == pytest.approx(0.1) + + +def test_sac_algorithm_update_target_network(): + config = create_default_config(state_dim=10, continuous_action_dim=6) + algo_config = SACAlgorithmConfig.from_policy_config(config) + algo_config.critic_target_update_weight = 1.0 + policy = GaussianActorPolicy(config=config) + algorithm = SACAlgorithm(policy=policy, config=algo_config) + + for p in algorithm.critic_ensemble.parameters(): + p.data = torch.ones_like(p.data) + + algorithm._update_target_networks() + for p in algorithm.critic_target.parameters(): + assert torch.allclose(p.data, torch.ones_like(p.data)) + + +@pytest.mark.parametrize("num_critics", [1, 3]) +def test_sac_algorithm_with_critics_number_of_heads(num_critics: int): + batch_size = 2 + action_dim = 10 + state_dim = 10 + config = create_config_with_visual_input(state_dim=state_dim, continuous_action_dim=action_dim) + + policy = GaussianActorPolicy(config=config) + policy.train() + algo_config = SACAlgorithmConfig.from_policy_config(config) + algo_config.num_critics = num_critics + algorithm = SACAlgorithm(policy=policy, config=algo_config) + algorithm.make_optimizers_and_scheduler() + + assert len(algorithm.critic_ensemble.critics) == num_critics + + batch = create_train_batch_with_visual_input( + batch_size=batch_size, state_dim=state_dim, action_dim=action_dim + ) + forward_batch = algorithm._prepare_forward_batch(batch) + + critic_loss = algorithm._compute_loss_critic(forward_batch) + assert critic_loss.shape == () + algorithm.optimizers["critic"].zero_grad() + critic_loss.backward() + algorithm.optimizers["critic"].step() + + +def test_gaussian_actor_policy_save_and_load(tmp_path): + """Test that the policy can be saved and loaded from pretrained.""" + root = tmp_path / "test_gaussian_actor_save_and_load" + + state_dim = 10 + action_dim = 10 + batch_size = 2 + + config = create_default_config(state_dim=state_dim, continuous_action_dim=action_dim) + policy = GaussianActorPolicy(config=config) + policy.eval() + policy.save_pretrained(root) + loaded_policy = GaussianActorPolicy.from_pretrained(root, config=config) + loaded_policy.eval() + + assert policy.state_dict().keys() == loaded_policy.state_dict().keys() + for k in policy.state_dict(): + assert torch.allclose(policy.state_dict()[k], loaded_policy.state_dict()[k], atol=1e-6) + + with torch.no_grad(): + with seeded_context(12): + observation_batch = create_observation_batch(batch_size=batch_size, state_dim=state_dim) + actions = policy.select_action(observation_batch) + + with seeded_context(12): + loaded_observation_batch = create_observation_batch(batch_size=batch_size, state_dim=state_dim) + loaded_actions = loaded_policy.select_action(loaded_observation_batch) + + assert torch.allclose(actions, loaded_actions) + + +def test_gaussian_actor_policy_save_and_load_with_discrete_critic(tmp_path): + """Discrete critic should be saved/loaded as part of the policy.""" + root = tmp_path / "test_gaussian_actor_save_and_load_discrete" + + state_dim = 10 + action_dim = 6 + + config = create_default_config(state_dim=state_dim, continuous_action_dim=action_dim) + config.num_discrete_actions = 3 + policy = GaussianActorPolicy(config=config) + policy.eval() + policy.save_pretrained(root) + + loaded_policy = GaussianActorPolicy.from_pretrained(root, config=config) + loaded_policy.eval() + + assert loaded_policy.discrete_critic is not None + dc_keys = [k for k in loaded_policy.state_dict() if k.startswith("discrete_critic.")] + assert len(dc_keys) > 0 + + for k in policy.state_dict(): + assert torch.allclose(policy.state_dict()[k], loaded_policy.state_dict()[k], atol=1e-6) diff --git a/tests/policies/test_sac_policy.py b/tests/policies/test_sac_policy.py deleted file mode 100644 index 11499ce30..000000000 --- a/tests/policies/test_sac_policy.py +++ /dev/null @@ -1,546 +0,0 @@ -# !/usr/bin/env python - -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math - -import pytest -import torch -from torch import Tensor, nn - -from lerobot.configs.types import FeatureType, PolicyFeature -from lerobot.policies.sac.configuration_sac import SACConfig -from lerobot.policies.sac.modeling_sac import MLP, SACPolicy -from lerobot.utils.constants import ACTION, OBS_IMAGE, OBS_STATE -from lerobot.utils.random_utils import seeded_context, set_seed - -try: - import transformers # noqa: F401 - - TRANSFORMERS_AVAILABLE = True -except ImportError: - TRANSFORMERS_AVAILABLE = False - - -@pytest.fixture(autouse=True) -def set_random_seed(): - seed = 42 - set_seed(seed) - - -def test_mlp_with_default_args(): - mlp = MLP(input_dim=10, hidden_dims=[256, 256]) - - x = torch.randn(10) - y = mlp(x) - assert y.shape == (256,) - - -def test_mlp_with_batch_dim(): - mlp = MLP(input_dim=10, hidden_dims=[256, 256]) - x = torch.randn(2, 10) - y = mlp(x) - assert y.shape == (2, 256) - - -def test_forward_with_empty_hidden_dims(): - mlp = MLP(input_dim=10, hidden_dims=[]) - x = torch.randn(1, 10) - assert mlp(x).shape == (1, 10) - - -def test_mlp_with_dropout(): - mlp = MLP(input_dim=10, hidden_dims=[256, 256, 11], dropout_rate=0.1) - x = torch.randn(1, 10) - y = mlp(x) - assert y.shape == (1, 11) - - drop_out_layers_count = sum(isinstance(layer, nn.Dropout) for layer in mlp.net) - assert drop_out_layers_count == 2 - - -def test_mlp_with_custom_final_activation(): - mlp = MLP(input_dim=10, hidden_dims=[256, 256], final_activation=torch.nn.Tanh()) - x = torch.randn(1, 10) - y = mlp(x) - assert y.shape == (1, 256) - assert (y >= -1).all() and (y <= 1).all() - - -def test_sac_policy_with_default_args(): - with pytest.raises(ValueError, match="should be an instance of class `PreTrainedConfig`"): - SACPolicy() - - -def create_dummy_state(batch_size: int, state_dim: int = 10) -> Tensor: - return { - OBS_STATE: torch.randn(batch_size, state_dim), - } - - -def create_dummy_with_visual_input(batch_size: int, state_dim: int = 10) -> Tensor: - return { - OBS_IMAGE: torch.randn(batch_size, 3, 84, 84), - OBS_STATE: torch.randn(batch_size, state_dim), - } - - -def create_dummy_action(batch_size: int, action_dim: int = 10) -> Tensor: - return torch.randn(batch_size, action_dim) - - -def create_default_train_batch( - batch_size: int = 8, state_dim: int = 10, action_dim: int = 10 -) -> dict[str, Tensor]: - return { - ACTION: create_dummy_action(batch_size, action_dim), - "reward": torch.randn(batch_size), - "state": create_dummy_state(batch_size, state_dim), - "next_state": create_dummy_state(batch_size, state_dim), - "done": torch.randn(batch_size), - } - - -def create_train_batch_with_visual_input( - batch_size: int = 8, state_dim: int = 10, action_dim: int = 10 -) -> dict[str, Tensor]: - return { - ACTION: create_dummy_action(batch_size, action_dim), - "reward": torch.randn(batch_size), - "state": create_dummy_with_visual_input(batch_size, state_dim), - "next_state": create_dummy_with_visual_input(batch_size, state_dim), - "done": torch.randn(batch_size), - } - - -def create_observation_batch(batch_size: int = 8, state_dim: int = 10) -> dict[str, Tensor]: - return { - OBS_STATE: torch.randn(batch_size, state_dim), - } - - -def create_observation_batch_with_visual_input(batch_size: int = 8, state_dim: int = 10) -> dict[str, Tensor]: - return { - OBS_STATE: torch.randn(batch_size, state_dim), - OBS_IMAGE: torch.randn(batch_size, 3, 84, 84), - } - - -def make_optimizers(policy: SACPolicy, has_discrete_action: bool = False) -> dict[str, torch.optim.Optimizer]: - """Create optimizers for the SAC policy.""" - optimizer_actor = torch.optim.Adam( - # Handle the case of shared encoder where the encoder weights are not optimized with the actor gradient - params=[ - p - for n, p in policy.actor.named_parameters() - if not policy.config.shared_encoder or not n.startswith("encoder") - ], - lr=policy.config.actor_lr, - ) - optimizer_critic = torch.optim.Adam( - params=policy.critic_ensemble.parameters(), - lr=policy.config.critic_lr, - ) - optimizer_temperature = torch.optim.Adam( - params=[policy.log_alpha], - lr=policy.config.critic_lr, - ) - - optimizers = { - "actor": optimizer_actor, - "critic": optimizer_critic, - "temperature": optimizer_temperature, - } - - if has_discrete_action: - optimizers["discrete_critic"] = torch.optim.Adam( - params=policy.discrete_critic.parameters(), - lr=policy.config.critic_lr, - ) - - return optimizers - - -def create_default_config( - state_dim: int, continuous_action_dim: int, has_discrete_action: bool = False -) -> SACConfig: - action_dim = continuous_action_dim - if has_discrete_action: - action_dim += 1 - - config = SACConfig( - input_features={OBS_STATE: PolicyFeature(type=FeatureType.STATE, shape=(state_dim,))}, - output_features={ACTION: PolicyFeature(type=FeatureType.ACTION, shape=(continuous_action_dim,))}, - dataset_stats={ - OBS_STATE: { - "min": [0.0] * state_dim, - "max": [1.0] * state_dim, - }, - ACTION: { - "min": [0.0] * continuous_action_dim, - "max": [1.0] * continuous_action_dim, - }, - }, - ) - config.validate_features() - return config - - -def create_config_with_visual_input( - state_dim: int, continuous_action_dim: int, has_discrete_action: bool = False -) -> SACConfig: - config = create_default_config( - state_dim=state_dim, - continuous_action_dim=continuous_action_dim, - has_discrete_action=has_discrete_action, - ) - config.input_features[OBS_IMAGE] = PolicyFeature(type=FeatureType.VISUAL, shape=(3, 84, 84)) - config.dataset_stats[OBS_IMAGE] = { - "mean": torch.randn(3, 1, 1), - "std": torch.randn(3, 1, 1), - } - - # Let make tests a little bit faster - config.state_encoder_hidden_dim = 32 - config.latent_dim = 32 - - config.validate_features() - return config - - -@pytest.mark.parametrize("batch_size,state_dim,action_dim", [(2, 6, 6), (1, 10, 10)]) -def test_sac_policy_with_default_config(batch_size: int, state_dim: int, action_dim: int): - batch = create_default_train_batch(batch_size=batch_size, action_dim=action_dim, state_dim=state_dim) - config = create_default_config(state_dim=state_dim, continuous_action_dim=action_dim) - - policy = SACPolicy(config=config) - policy.train() - - optimizers = make_optimizers(policy) - - cirtic_loss = policy.forward(batch, model="critic")["loss_critic"] - assert cirtic_loss.item() is not None - assert cirtic_loss.shape == () - cirtic_loss.backward() - optimizers["critic"].step() - - actor_loss = policy.forward(batch, model="actor")["loss_actor"] - assert actor_loss.item() is not None - assert actor_loss.shape == () - - actor_loss.backward() - optimizers["actor"].step() - - temperature_loss = policy.forward(batch, model="temperature")["loss_temperature"] - assert temperature_loss.item() is not None - assert temperature_loss.shape == () - - temperature_loss.backward() - optimizers["temperature"].step() - - policy.eval() - with torch.no_grad(): - observation_batch = create_observation_batch(batch_size=batch_size, state_dim=state_dim) - selected_action = policy.select_action(observation_batch) - assert selected_action.shape == (batch_size, action_dim) - - -@pytest.mark.parametrize("batch_size,state_dim,action_dim", [(2, 6, 6), (1, 10, 10)]) -def test_sac_policy_with_visual_input(batch_size: int, state_dim: int, action_dim: int): - config = create_config_with_visual_input(state_dim=state_dim, continuous_action_dim=action_dim) - policy = SACPolicy(config=config) - - batch = create_train_batch_with_visual_input( - batch_size=batch_size, state_dim=state_dim, action_dim=action_dim - ) - - policy.train() - - optimizers = make_optimizers(policy) - - cirtic_loss = policy.forward(batch, model="critic")["loss_critic"] - assert cirtic_loss.item() is not None - assert cirtic_loss.shape == () - cirtic_loss.backward() - optimizers["critic"].step() - - actor_loss = policy.forward(batch, model="actor")["loss_actor"] - assert actor_loss.item() is not None - assert actor_loss.shape == () - - actor_loss.backward() - optimizers["actor"].step() - - temperature_loss = policy.forward(batch, model="temperature")["loss_temperature"] - assert temperature_loss.item() is not None - assert temperature_loss.shape == () - - temperature_loss.backward() - optimizers["temperature"].step() - - policy.eval() - with torch.no_grad(): - observation_batch = create_observation_batch_with_visual_input( - batch_size=batch_size, state_dim=state_dim - ) - selected_action = policy.select_action(observation_batch) - assert selected_action.shape == (batch_size, action_dim) - - -# Let's check best candidates for pretrained encoders -@pytest.mark.parametrize( - "batch_size,state_dim,action_dim,vision_encoder_name", - [(1, 6, 6, "helper2424/resnet10"), (1, 6, 6, "facebook/convnext-base-224")], -) -@pytest.mark.skipif(not TRANSFORMERS_AVAILABLE, reason="Transformers are not installed") -@pytest.mark.skip( - reason="helper2424/resnet10 needs to be updated to work with the latest version of transformers" -) -def test_sac_policy_with_pretrained_encoder( - batch_size: int, state_dim: int, action_dim: int, vision_encoder_name: str -): - config = create_config_with_visual_input(state_dim=state_dim, continuous_action_dim=action_dim) - config.vision_encoder_name = vision_encoder_name - policy = SACPolicy(config=config) - policy.train() - - batch = create_train_batch_with_visual_input( - batch_size=batch_size, state_dim=state_dim, action_dim=action_dim - ) - - optimizers = make_optimizers(policy) - - cirtic_loss = policy.forward(batch, model="critic")["loss_critic"] - assert cirtic_loss.item() is not None - assert cirtic_loss.shape == () - cirtic_loss.backward() - optimizers["critic"].step() - - actor_loss = policy.forward(batch, model="actor")["loss_actor"] - assert actor_loss.item() is not None - assert actor_loss.shape == () - - -def test_sac_policy_with_shared_encoder(): - batch_size = 2 - action_dim = 10 - state_dim = 10 - config = create_config_with_visual_input(state_dim=state_dim, continuous_action_dim=action_dim) - config.shared_encoder = True - - policy = SACPolicy(config=config) - policy.train() - - batch = create_train_batch_with_visual_input( - batch_size=batch_size, state_dim=state_dim, action_dim=action_dim - ) - - policy.train() - - optimizers = make_optimizers(policy) - - cirtic_loss = policy.forward(batch, model="critic")["loss_critic"] - assert cirtic_loss.item() is not None - assert cirtic_loss.shape == () - cirtic_loss.backward() - optimizers["critic"].step() - - actor_loss = policy.forward(batch, model="actor")["loss_actor"] - assert actor_loss.item() is not None - assert actor_loss.shape == () - - actor_loss.backward() - optimizers["actor"].step() - - -def test_sac_policy_with_discrete_critic(): - batch_size = 2 - continuous_action_dim = 9 - full_action_dim = continuous_action_dim + 1 # the last action is discrete - state_dim = 10 - config = create_config_with_visual_input( - state_dim=state_dim, continuous_action_dim=continuous_action_dim, has_discrete_action=True - ) - - num_discrete_actions = 5 - config.num_discrete_actions = num_discrete_actions - - policy = SACPolicy(config=config) - policy.train() - - batch = create_train_batch_with_visual_input( - batch_size=batch_size, state_dim=state_dim, action_dim=full_action_dim - ) - - policy.train() - - optimizers = make_optimizers(policy, has_discrete_action=True) - - cirtic_loss = policy.forward(batch, model="critic")["loss_critic"] - assert cirtic_loss.item() is not None - assert cirtic_loss.shape == () - cirtic_loss.backward() - optimizers["critic"].step() - - discrete_critic_loss = policy.forward(batch, model="discrete_critic")["loss_discrete_critic"] - assert discrete_critic_loss.item() is not None - assert discrete_critic_loss.shape == () - discrete_critic_loss.backward() - optimizers["discrete_critic"].step() - - actor_loss = policy.forward(batch, model="actor")["loss_actor"] - assert actor_loss.item() is not None - assert actor_loss.shape == () - - actor_loss.backward() - optimizers["actor"].step() - - policy.eval() - with torch.no_grad(): - observation_batch = create_observation_batch_with_visual_input( - batch_size=batch_size, state_dim=state_dim - ) - selected_action = policy.select_action(observation_batch) - assert selected_action.shape == (batch_size, full_action_dim) - - discrete_actions = selected_action[:, -1].long() - discrete_action_values = set(discrete_actions.tolist()) - - assert all(action in range(num_discrete_actions) for action in discrete_action_values), ( - f"Discrete action {discrete_action_values} is not in range({num_discrete_actions})" - ) - - -def test_sac_policy_with_default_entropy(): - config = create_default_config(continuous_action_dim=10, state_dim=10) - policy = SACPolicy(config=config) - assert policy.target_entropy == -5.0 - - -def test_sac_policy_default_target_entropy_with_discrete_action(): - config = create_config_with_visual_input(state_dim=10, continuous_action_dim=6, has_discrete_action=True) - policy = SACPolicy(config=config) - assert policy.target_entropy == -3.0 - - -def test_sac_policy_with_predefined_entropy(): - config = create_default_config(state_dim=10, continuous_action_dim=6) - config.target_entropy = -3.5 - - policy = SACPolicy(config=config) - assert policy.target_entropy == pytest.approx(-3.5) - - -def test_sac_policy_update_temperature(): - """Test that temperature property is always in sync with log_alpha.""" - config = create_default_config(continuous_action_dim=10, state_dim=10) - policy = SACPolicy(config=config) - - assert policy.temperature == pytest.approx(1.0) - policy.log_alpha.data = torch.tensor([math.log(0.1)]) - # Temperature property automatically reflects log_alpha changes - assert policy.temperature == pytest.approx(0.1) - - -def test_sac_policy_update_target_network(): - config = create_default_config(state_dim=10, continuous_action_dim=6) - config.critic_target_update_weight = 1.0 - - policy = SACPolicy(config=config) - policy.train() - - for p in policy.critic_ensemble.parameters(): - p.data = torch.ones_like(p.data) - - policy.update_target_networks() - for p in policy.critic_target.parameters(): - assert torch.allclose(p.data, torch.ones_like(p.data)), ( - f"Target network {p.data} is not equal to {torch.ones_like(p.data)}" - ) - - -@pytest.mark.parametrize("num_critics", [1, 3]) -def test_sac_policy_with_critics_number_of_heads(num_critics: int): - batch_size = 2 - action_dim = 10 - state_dim = 10 - config = create_config_with_visual_input(state_dim=state_dim, continuous_action_dim=action_dim) - config.num_critics = num_critics - - policy = SACPolicy(config=config) - policy.train() - - assert len(policy.critic_ensemble.critics) == num_critics - - batch = create_train_batch_with_visual_input( - batch_size=batch_size, state_dim=state_dim, action_dim=action_dim - ) - - policy.train() - - optimizers = make_optimizers(policy) - - cirtic_loss = policy.forward(batch, model="critic")["loss_critic"] - assert cirtic_loss.item() is not None - assert cirtic_loss.shape == () - cirtic_loss.backward() - optimizers["critic"].step() - - -def test_sac_policy_save_and_load(tmp_path): - root = tmp_path / "test_sac_save_and_load" - - state_dim = 10 - action_dim = 10 - batch_size = 2 - - config = create_default_config(state_dim=state_dim, continuous_action_dim=action_dim) - policy = SACPolicy(config=config) - policy.eval() - policy.save_pretrained(root) - loaded_policy = SACPolicy.from_pretrained(root, config=config) - loaded_policy.eval() - - batch = create_default_train_batch(batch_size=1, state_dim=10, action_dim=10) - - with torch.no_grad(): - with seeded_context(12): - # Collect policy values before saving - cirtic_loss = policy.forward(batch, model="critic")["loss_critic"] - actor_loss = policy.forward(batch, model="actor")["loss_actor"] - temperature_loss = policy.forward(batch, model="temperature")["loss_temperature"] - - observation_batch = create_observation_batch(batch_size=batch_size, state_dim=state_dim) - actions = policy.select_action(observation_batch) - - with seeded_context(12): - # Collect policy values after loading - loaded_cirtic_loss = loaded_policy.forward(batch, model="critic")["loss_critic"] - loaded_actor_loss = loaded_policy.forward(batch, model="actor")["loss_actor"] - loaded_temperature_loss = loaded_policy.forward(batch, model="temperature")["loss_temperature"] - - loaded_observation_batch = create_observation_batch(batch_size=batch_size, state_dim=state_dim) - loaded_actions = loaded_policy.select_action(loaded_observation_batch) - - assert policy.state_dict().keys() == loaded_policy.state_dict().keys() - for k in policy.state_dict(): - assert torch.allclose(policy.state_dict()[k], loaded_policy.state_dict()[k], atol=1e-6) - - # Compare values before and after saving and loading - # They should be the same - assert torch.allclose(cirtic_loss, loaded_cirtic_loss) - assert torch.allclose(actor_loss, loaded_actor_loss) - assert torch.allclose(temperature_loss, loaded_temperature_loss) - assert torch.allclose(actions, loaded_actions) diff --git a/tests/processor/test_sac_processor.py b/tests/processor/test_gaussian_actor_processor.py similarity index 89% rename from tests/processor/test_sac_processor.py rename to tests/processor/test_gaussian_actor_processor.py index a1a4b285d..2429bc23a 100644 --- a/tests/processor/test_sac_processor.py +++ b/tests/processor/test_gaussian_actor_processor.py @@ -21,8 +21,8 @@ import pytest import torch from lerobot.configs.types import FeatureType, NormalizationMode, PolicyFeature -from lerobot.policies.sac.configuration_sac import SACConfig -from lerobot.policies.sac.processor_sac import make_sac_pre_post_processors +from lerobot.policies.gaussian_actor.configuration_gaussian_actor import GaussianActorConfig +from lerobot.policies.gaussian_actor.processor_gaussian_actor import make_gaussian_actor_pre_post_processors from lerobot.processor import ( AddBatchDimensionProcessorStep, DataProcessorPipeline, @@ -38,7 +38,7 @@ from lerobot.utils.constants import ACTION, OBS_STATE def create_default_config(): """Create a default SAC configuration for testing.""" - config = SACConfig() + config = GaussianActorConfig() config.input_features = { OBS_STATE: PolicyFeature(type=FeatureType.STATE, shape=(10,)), } @@ -66,7 +66,7 @@ def test_make_sac_processor_basic(): config = create_default_config() stats = create_default_stats() - preprocessor, postprocessor = make_sac_pre_post_processors( + preprocessor, postprocessor = make_gaussian_actor_pre_post_processors( config, stats, ) @@ -88,12 +88,12 @@ def test_make_sac_processor_basic(): assert isinstance(postprocessor.steps[1], DeviceProcessorStep) -def test_sac_processor_normalization_modes(): +def test_gaussian_actor_processor_normalization_modes(): """Test that SAC processor correctly handles different normalization modes.""" config = create_default_config() stats = create_default_stats() - preprocessor, postprocessor = make_sac_pre_post_processors( + preprocessor, postprocessor = make_gaussian_actor_pre_post_processors( config, stats, ) @@ -121,13 +121,13 @@ def test_sac_processor_normalization_modes(): @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") -def test_sac_processor_cuda(): +def test_gaussian_actor_processor_cuda(): """Test SAC processor with CUDA device.""" config = create_default_config() config.device = "cuda" stats = create_default_stats() - preprocessor, postprocessor = make_sac_pre_post_processors( + preprocessor, postprocessor = make_gaussian_actor_pre_post_processors( config, stats, ) @@ -153,13 +153,13 @@ def test_sac_processor_cuda(): @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") -def test_sac_processor_accelerate_scenario(): +def test_gaussian_actor_processor_accelerate_scenario(): """Test SAC processor in simulated Accelerate scenario.""" config = create_default_config() config.device = "cuda:0" stats = create_default_stats() - preprocessor, postprocessor = make_sac_pre_post_processors( + preprocessor, postprocessor = make_gaussian_actor_pre_post_processors( config, stats, ) @@ -180,13 +180,13 @@ def test_sac_processor_accelerate_scenario(): @pytest.mark.skipif(torch.cuda.device_count() < 2, reason="Requires at least 2 GPUs") -def test_sac_processor_multi_gpu(): +def test_gaussian_actor_processor_multi_gpu(): """Test SAC processor with multi-GPU setup.""" config = create_default_config() config.device = "cuda:0" stats = create_default_stats() - preprocessor, postprocessor = make_sac_pre_post_processors( + preprocessor, postprocessor = make_gaussian_actor_pre_post_processors( config, stats, ) @@ -206,11 +206,11 @@ def test_sac_processor_multi_gpu(): assert processed[TransitionKey.ACTION.value].device == device -def test_sac_processor_without_stats(): +def test_gaussian_actor_processor_without_stats(): """Test SAC processor creation without dataset statistics.""" config = create_default_config() - preprocessor, postprocessor = make_sac_pre_post_processors(config, dataset_stats=None) + preprocessor, postprocessor = make_gaussian_actor_pre_post_processors(config, dataset_stats=None) # Should still create processors assert preprocessor is not None @@ -226,12 +226,12 @@ def test_sac_processor_without_stats(): assert processed is not None -def test_sac_processor_save_and_load(): +def test_gaussian_actor_processor_save_and_load(): """Test saving and loading SAC processor.""" config = create_default_config() stats = create_default_stats() - preprocessor, postprocessor = make_sac_pre_post_processors( + preprocessor, postprocessor = make_gaussian_actor_pre_post_processors( config, stats, ) @@ -257,14 +257,14 @@ def test_sac_processor_save_and_load(): @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") -def test_sac_processor_mixed_precision(): +def test_gaussian_actor_processor_mixed_precision(): """Test SAC processor with mixed precision.""" config = create_default_config() config.device = "cuda" stats = create_default_stats() # Create processor - preprocessor, postprocessor = make_sac_pre_post_processors( + preprocessor, postprocessor = make_gaussian_actor_pre_post_processors( config, stats, ) @@ -304,12 +304,12 @@ def test_sac_processor_mixed_precision(): assert processed[TransitionKey.ACTION.value].dtype == torch.float16 -def test_sac_processor_batch_data(): +def test_gaussian_actor_processor_batch_data(): """Test SAC processor with batched data.""" config = create_default_config() stats = create_default_stats() - preprocessor, postprocessor = make_sac_pre_post_processors( + preprocessor, postprocessor = make_gaussian_actor_pre_post_processors( config, stats, ) @@ -329,12 +329,12 @@ def test_sac_processor_batch_data(): assert processed[TransitionKey.ACTION.value].shape == (batch_size, 5) -def test_sac_processor_edge_cases(): +def test_gaussian_actor_processor_edge_cases(): """Test SAC processor with edge cases.""" config = create_default_config() stats = create_default_stats() - preprocessor, postprocessor = make_sac_pre_post_processors( + preprocessor, postprocessor = make_gaussian_actor_pre_post_processors( config, stats, ) @@ -358,13 +358,13 @@ def test_sac_processor_edge_cases(): @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") -def test_sac_processor_bfloat16_device_float32_normalizer(): +def test_gaussian_actor_processor_bfloat16_device_float32_normalizer(): """Test: DeviceProcessor(bfloat16) + NormalizerProcessor(float32) → output bfloat16 via automatic adaptation""" config = create_default_config() config.device = "cuda" stats = create_default_stats() - preprocessor, _ = make_sac_pre_post_processors( + preprocessor, _ = make_gaussian_actor_pre_post_processors( config, stats, ) diff --git a/tests/processor/test_normalize_processor.py b/tests/processor/test_normalize_processor.py index cd5c75005..e046adb0d 100644 --- a/tests/processor/test_normalize_processor.py +++ b/tests/processor/test_normalize_processor.py @@ -1804,13 +1804,15 @@ def test_stats_override_preservation_in_load_state_dict(): override_normalizer.stats[key][stat_name], original_stats[key][stat_name] ), f"Stats for {key}.{stat_name} should not match original stats" - # Verify that _tensor_stats are also correctly set to match the override stats + # Verify that _tensor_stats values match the override stats + # Note: visual stats are reshaped from (C,) to (C,1,1) by _reshape_visual_stats expected_tensor_stats = to_tensor(override_stats) for key in expected_tensor_stats: for stat_name in expected_tensor_stats[key]: if isinstance(expected_tensor_stats[key][stat_name], torch.Tensor): torch.testing.assert_close( - override_normalizer._tensor_stats[key][stat_name], expected_tensor_stats[key][stat_name] + override_normalizer._tensor_stats[key][stat_name].squeeze(), + expected_tensor_stats[key][stat_name].squeeze(), ) @@ -1849,12 +1851,16 @@ def test_stats_without_override_loads_normally(): # Stats should now match the original stats (normal behavior) # Check that all keys and values match assert set(new_normalizer.stats.keys()) == set(original_stats.keys()) + # Note: visual stats are reshaped from (C,) to (C,1,1) by _reshape_visual_stats, + # so we squeeze before comparing values. for key in original_stats: assert set(new_normalizer.stats[key].keys()) == set(original_stats[key].keys()) for stat_name in original_stats[key]: - np.testing.assert_allclose( - new_normalizer.stats[key][stat_name], original_stats[key][stat_name], rtol=1e-6, atol=1e-6 - ) + actual = new_normalizer.stats[key][stat_name] + expected = original_stats[key][stat_name] + if hasattr(actual, "squeeze"): + actual = actual.squeeze() + np.testing.assert_allclose(actual, expected, rtol=1e-6, atol=1e-6) def test_stats_explicit_provided_flag_detection(): @@ -2075,8 +2081,9 @@ def test_stats_reconstruction_after_load_state_dict(): assert ACTION in new_normalizer.stats # Check that values are correct (converted back from tensors) - np.testing.assert_allclose(new_normalizer.stats[OBS_IMAGE]["mean"], [0.5, 0.5, 0.5]) - np.testing.assert_allclose(new_normalizer.stats[OBS_IMAGE]["std"], [0.2, 0.2, 0.2]) + # Note: visual stats are reshaped to (C,1,1), so we squeeze before comparing + np.testing.assert_allclose(new_normalizer.stats[OBS_IMAGE]["mean"].squeeze(), [0.5, 0.5, 0.5]) + np.testing.assert_allclose(new_normalizer.stats[OBS_IMAGE]["std"].squeeze(), [0.2, 0.2, 0.2]) np.testing.assert_allclose(new_normalizer.stats[OBS_STATE]["min"], [0.0, -1.0]) np.testing.assert_allclose(new_normalizer.stats[OBS_STATE]["max"], [1.0, 1.0]) np.testing.assert_allclose(new_normalizer.stats[ACTION]["mean"], [0.0, 0.0]) diff --git a/tests/rewards/test_modeling_classifier.py b/tests/rewards/test_modeling_classifier.py index 08f6121a1..043dbb660 100644 --- a/tests/rewards/test_modeling_classifier.py +++ b/tests/rewards/test_modeling_classifier.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pytest import torch from lerobot.configs.types import FeatureType, NormalizationMode, PolicyFeature @@ -36,9 +35,6 @@ def test_classifier_output(): @skip_if_package_missing("transformers") -@pytest.mark.skip( - reason="helper2424/resnet10 needs to be updated to work with the latest version of transformers" -) def test_binary_classifier_with_default_params(): from lerobot.rewards.classifier.modeling_classifier import Classifier @@ -80,9 +76,6 @@ def test_binary_classifier_with_default_params(): @skip_if_package_missing("transformers") -@pytest.mark.skip( - reason="helper2424/resnet10 needs to be updated to work with the latest version of transformers" -) def test_multiclass_classifier(): from lerobot.rewards.classifier.modeling_classifier import Classifier @@ -122,9 +115,6 @@ def test_multiclass_classifier(): @skip_if_package_missing("transformers") -@pytest.mark.skip( - reason="helper2424/resnet10 needs to be updated to work with the latest version of transformers" -) def test_default_device(): from lerobot.rewards.classifier.modeling_classifier import Classifier @@ -141,9 +131,6 @@ def test_default_device(): @skip_if_package_missing("transformers") -@pytest.mark.skip( - reason="helper2424/resnet10 needs to be updated to work with the latest version of transformers" -) def test_explicit_device_setup(): from lerobot.rewards.classifier.modeling_classifier import Classifier diff --git a/tests/rl/test_actor_learner.py b/tests/rl/test_actor_learner.py index 3978dfffd..e0df14e62 100644 --- a/tests/rl/test_actor_learner.py +++ b/tests/rl/test_actor_learner.py @@ -22,12 +22,14 @@ import pytest import torch pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])") +pytest.importorskip("grpc") from torch.multiprocessing import Event, Queue -from lerobot.configs.train import TrainRLServerPipelineConfig -from lerobot.policies.sac.configuration_sac import SACConfig -from lerobot.utils.constants import OBS_STR +from lerobot.configs.types import FeatureType, PolicyFeature +from lerobot.policies.gaussian_actor.configuration_gaussian_actor import GaussianActorConfig +from lerobot.rl.train_rl import TrainRLServerPipelineConfig +from lerobot.utils.constants import ACTION, OBS_STATE, OBS_STR from lerobot.utils.transition import Transition from tests.utils import skip_if_package_missing @@ -79,7 +81,7 @@ def cfg(): port = find_free_port() - policy_cfg = SACConfig() + policy_cfg = GaussianActorConfig() policy_cfg.actor_learner_config.learner_host = "127.0.0.1" policy_cfg.actor_learner_config.learner_port = port policy_cfg.concurrency.actor = "threads" @@ -299,3 +301,164 @@ def test_end_to_end_parameters_flow(cfg, data_size): assert received_params.keys() == input_params.keys() for key in input_params: assert torch.allclose(received_params[key], input_params[key]) + + +def test_learner_algorithm_wiring(): + """Verify that make_algorithm constructs an SACAlgorithm from config, + make_optimizers_and_scheduler() creates the right optimizers, update() works, and + get_weights() output is serializable.""" + from lerobot.policies.gaussian_actor.modeling_gaussian_actor import GaussianActorPolicy + from lerobot.rl.algorithms.factory import make_algorithm + from lerobot.rl.algorithms.sac import SACAlgorithm, SACAlgorithmConfig + from lerobot.transport.utils import state_to_bytes + + state_dim = 10 + action_dim = 6 + + sac_cfg = GaussianActorConfig( + input_features={OBS_STATE: PolicyFeature(type=FeatureType.STATE, shape=(state_dim,))}, + output_features={ACTION: PolicyFeature(type=FeatureType.ACTION, shape=(action_dim,))}, + dataset_stats={ + OBS_STATE: {"min": [0.0] * state_dim, "max": [1.0] * state_dim}, + ACTION: {"min": [0.0] * action_dim, "max": [1.0] * action_dim}, + }, + ) + sac_cfg.validate_features() + + policy = GaussianActorPolicy(config=sac_cfg) + policy.train() + + algorithm = make_algorithm(cfg=SACAlgorithmConfig.from_policy_config(sac_cfg), policy=policy) + assert isinstance(algorithm, SACAlgorithm) + + optimizers = algorithm.make_optimizers_and_scheduler() + assert "actor" in optimizers + assert "critic" in optimizers + assert "temperature" in optimizers + + batch_size = 4 + + def batch_iterator(): + while True: + yield { + ACTION: torch.randn(batch_size, action_dim), + "reward": torch.randn(batch_size), + "state": {OBS_STATE: torch.randn(batch_size, state_dim)}, + "next_state": {OBS_STATE: torch.randn(batch_size, state_dim)}, + "done": torch.zeros(batch_size), + "complementary_info": {}, + } + + stats = algorithm.update(batch_iterator()) + assert "loss_critic" in stats.losses + + # get_weights -> state_to_bytes round-trip + weights = algorithm.get_weights() + assert len(weights) > 0 + serialized = state_to_bytes(weights) + assert isinstance(serialized, bytes) + assert len(serialized) > 0 + + # RLTrainer with DataMixer + from lerobot.rl.buffer import ReplayBuffer + from lerobot.rl.data_sources import OnlineOfflineMixer + from lerobot.rl.trainer import RLTrainer + + replay_buffer = ReplayBuffer( + capacity=50, + device="cpu", + state_keys=[OBS_STATE], + storage_device="cpu", + use_drq=False, + ) + for _ in range(50): + replay_buffer.add( + state={OBS_STATE: torch.randn(state_dim)}, + action=torch.randn(action_dim), + reward=1.0, + next_state={OBS_STATE: torch.randn(state_dim)}, + done=False, + truncated=False, + ) + data_mixer = OnlineOfflineMixer(online_buffer=replay_buffer, offline_buffer=None) + trainer = RLTrainer( + algorithm=algorithm, + data_mixer=data_mixer, + batch_size=batch_size, + ) + trainer_stats = trainer.training_step() + assert "loss_critic" in trainer_stats.losses + + +def test_initial_and_periodic_weight_push_consistency(): + """Both initial and periodic weight pushes should use algorithm.get_weights() + and produce identical structures.""" + from lerobot.policies.gaussian_actor.modeling_gaussian_actor import GaussianActorPolicy + from lerobot.rl.algorithms.factory import make_algorithm + from lerobot.rl.algorithms.sac import SACAlgorithmConfig + from lerobot.transport.utils import bytes_to_state_dict, state_to_bytes + + state_dim = 10 + action_dim = 6 + sac_cfg = GaussianActorConfig( + input_features={OBS_STATE: PolicyFeature(type=FeatureType.STATE, shape=(state_dim,))}, + output_features={ACTION: PolicyFeature(type=FeatureType.ACTION, shape=(action_dim,))}, + dataset_stats={ + OBS_STATE: {"min": [0.0] * state_dim, "max": [1.0] * state_dim}, + ACTION: {"min": [0.0] * action_dim, "max": [1.0] * action_dim}, + }, + ) + sac_cfg.validate_features() + + policy = GaussianActorPolicy(config=sac_cfg) + policy.train() + algorithm = make_algorithm(cfg=SACAlgorithmConfig.from_policy_config(sac_cfg), policy=policy) + algorithm.make_optimizers_and_scheduler() + + # Simulate initial push (same code path the learner now uses) + initial_weights = algorithm.get_weights() + initial_bytes = state_to_bytes(initial_weights) + + # Simulate periodic push + periodic_weights = algorithm.get_weights() + periodic_bytes = state_to_bytes(periodic_weights) + + initial_decoded = bytes_to_state_dict(initial_bytes) + periodic_decoded = bytes_to_state_dict(periodic_bytes) + + assert initial_decoded.keys() == periodic_decoded.keys() + + +def test_actor_side_algorithm_select_action_and_load_weights(): + """Simulate actor: create algorithm without optimizers, select_action, load_weights.""" + from lerobot.policies.gaussian_actor.modeling_gaussian_actor import GaussianActorPolicy + from lerobot.rl.algorithms.factory import make_algorithm + from lerobot.rl.algorithms.sac import SACAlgorithm, SACAlgorithmConfig + + state_dim = 10 + action_dim = 6 + sac_cfg = GaussianActorConfig( + input_features={OBS_STATE: PolicyFeature(type=FeatureType.STATE, shape=(state_dim,))}, + output_features={ACTION: PolicyFeature(type=FeatureType.ACTION, shape=(action_dim,))}, + dataset_stats={ + OBS_STATE: {"min": [0.0] * state_dim, "max": [1.0] * state_dim}, + ACTION: {"min": [0.0] * action_dim, "max": [1.0] * action_dim}, + }, + ) + sac_cfg.validate_features() + + # Actor side: no optimizers + policy = GaussianActorPolicy(config=sac_cfg) + policy.eval() + algorithm = make_algorithm(cfg=SACAlgorithmConfig.from_policy_config(sac_cfg), policy=policy) + assert isinstance(algorithm, SACAlgorithm) + assert algorithm.optimizers == {} + + # select_action should work + obs = {OBS_STATE: torch.randn(state_dim)} + action = policy.select_action(obs) + assert action.shape == (action_dim,) + + # Simulate receiving weights from learner + fake_weights = algorithm.get_weights() + algorithm.load_weights(fake_weights, device="cpu") diff --git a/tests/rl/test_data_mixer.py b/tests/rl/test_data_mixer.py new file mode 100644 index 000000000..b153498d7 --- /dev/null +++ b/tests/rl/test_data_mixer.py @@ -0,0 +1,89 @@ +# Copyright 2025 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for RL data mixing (DataMixer, OnlineOfflineMixer).""" + +import pytest + +pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])") + +import torch # noqa: E402 + +from lerobot.rl.buffer import ReplayBuffer # noqa: E402 +from lerobot.rl.data_sources import OnlineOfflineMixer # noqa: E402 +from lerobot.utils.constants import OBS_STATE # noqa: E402 + + +def _make_buffer(capacity: int = 100, state_dim: int = 4) -> ReplayBuffer: + buf = ReplayBuffer( + capacity=capacity, + device="cpu", + state_keys=[OBS_STATE], + storage_device="cpu", + use_drq=False, + ) + for i in range(capacity): + buf.add( + state={OBS_STATE: torch.randn(state_dim)}, + action=torch.randn(2), + reward=1.0, + next_state={OBS_STATE: torch.randn(state_dim)}, + done=bool(i % 10 == 9), + truncated=False, + ) + return buf + + +def test_online_only_mixer_sample(): + """OnlineOfflineMixer with no offline buffer returns online-only batches.""" + buf = _make_buffer(capacity=50) + mixer = OnlineOfflineMixer(online_buffer=buf, offline_buffer=None, online_ratio=0.5) + batch = mixer.sample(batch_size=8) + assert batch["state"][OBS_STATE].shape[0] == 8 + assert batch["action"].shape[0] == 8 + assert batch["reward"].shape[0] == 8 + + +def test_online_only_mixer_ratio_one(): + """OnlineOfflineMixer with online_ratio=1.0 and no offline is equivalent to online-only.""" + buf = _make_buffer(capacity=50) + mixer = OnlineOfflineMixer(online_buffer=buf, offline_buffer=None, online_ratio=1.0) + batch = mixer.sample(batch_size=10) + assert batch["state"][OBS_STATE].shape[0] == 10 + + +def test_online_offline_mixer_sample(): + """OnlineOfflineMixer with two buffers returns concatenated batches.""" + online = _make_buffer(capacity=50) + offline = _make_buffer(capacity=50) + mixer = OnlineOfflineMixer( + online_buffer=online, + offline_buffer=offline, + online_ratio=0.5, + ) + batch = mixer.sample(batch_size=10) + assert batch["state"][OBS_STATE].shape[0] == 10 + assert batch["action"].shape[0] == 10 + # 5 from online, 5 from offline (approx) + assert batch["reward"].shape[0] == 10 + + +def test_online_offline_mixer_iterator(): + """get_iterator yields batches of the requested size.""" + buf = _make_buffer(capacity=50) + mixer = OnlineOfflineMixer(online_buffer=buf, offline_buffer=None) + it = mixer.get_iterator(batch_size=4, async_prefetch=False) + batch1 = next(it) + batch2 = next(it) + assert batch1["state"][OBS_STATE].shape[0] == 4 + assert batch2["state"][OBS_STATE].shape[0] == 4 diff --git a/tests/rl/test_queue.py b/tests/rl/test_queue.py index cf3d6cdca..77936d269 100644 --- a/tests/rl/test_queue.py +++ b/tests/rl/test_queue.py @@ -20,7 +20,7 @@ from queue import Queue import pytest -pytest.importorskip("grpc") +pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])") from torch.multiprocessing import Queue as TorchMPQueue # noqa: E402 diff --git a/tests/rl/test_sac_algorithm.py b/tests/rl/test_sac_algorithm.py new file mode 100644 index 000000000..2d77ae9ba --- /dev/null +++ b/tests/rl/test_sac_algorithm.py @@ -0,0 +1,606 @@ +#!/usr/bin/env python + +# Copyright 2025 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the RL algorithm abstraction and SACAlgorithm implementation.""" + +import pytest + +pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])") + +import torch # noqa: E402 + +from lerobot.configs.types import FeatureType, PolicyFeature # noqa: E402 +from lerobot.policies.gaussian_actor.configuration_gaussian_actor import GaussianActorConfig # noqa: E402 +from lerobot.policies.gaussian_actor.modeling_gaussian_actor import GaussianActorPolicy # noqa: E402 +from lerobot.rl.algorithms.configs import RLAlgorithmConfig, TrainingStats # noqa: E402 +from lerobot.rl.algorithms.factory import make_algorithm # noqa: E402 +from lerobot.rl.algorithms.sac import SACAlgorithm, SACAlgorithmConfig # noqa: E402 +from lerobot.utils.constants import ACTION, OBS_IMAGE, OBS_STATE # noqa: E402 +from lerobot.utils.random_utils import set_seed # noqa: E402 + +# --------------------------------------------------------------------------- +# Helpers (reuse patterns from tests/policies/test_gaussian_actor_policy.py) +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def set_random_seed(): + set_seed(42) + + +def _make_sac_config( + state_dim: int = 10, + action_dim: int = 6, + num_discrete_actions: int | None = None, + with_images: bool = False, +) -> GaussianActorConfig: + config = GaussianActorConfig( + input_features={OBS_STATE: PolicyFeature(type=FeatureType.STATE, shape=(state_dim,))}, + output_features={ACTION: PolicyFeature(type=FeatureType.ACTION, shape=(action_dim,))}, + dataset_stats={ + OBS_STATE: {"min": [0.0] * state_dim, "max": [1.0] * state_dim}, + ACTION: {"min": [0.0] * action_dim, "max": [1.0] * action_dim}, + }, + num_discrete_actions=num_discrete_actions, + ) + if with_images: + config.input_features[OBS_IMAGE] = PolicyFeature(type=FeatureType.VISUAL, shape=(3, 84, 84)) + config.dataset_stats[OBS_IMAGE] = { + "mean": torch.randn(3, 1, 1).tolist(), + "std": torch.randn(3, 1, 1).abs().tolist(), + } + config.latent_dim = 32 + config.state_encoder_hidden_dim = 32 + config.validate_features() + return config + + +def _make_algorithm( + state_dim: int = 10, + action_dim: int = 6, + utd_ratio: int = 1, + policy_update_freq: int = 1, + num_discrete_actions: int | None = None, + with_images: bool = False, +) -> tuple[SACAlgorithm, GaussianActorPolicy]: + sac_cfg = _make_sac_config( + state_dim=state_dim, + action_dim=action_dim, + num_discrete_actions=num_discrete_actions, + with_images=with_images, + ) + policy = GaussianActorPolicy(config=sac_cfg) + policy.train() + algo_config = SACAlgorithmConfig.from_policy_config(sac_cfg) + algo_config.utd_ratio = utd_ratio + algo_config.policy_update_freq = policy_update_freq + algorithm = SACAlgorithm(policy=policy, config=algo_config) + algorithm.make_optimizers_and_scheduler() + return algorithm, policy + + +def _make_batch( + batch_size: int = 4, + state_dim: int = 10, + action_dim: int = 6, + with_images: bool = False, +) -> dict: + obs = {OBS_STATE: torch.randn(batch_size, state_dim)} + next_obs = {OBS_STATE: torch.randn(batch_size, state_dim)} + if with_images: + obs[OBS_IMAGE] = torch.randn(batch_size, 3, 84, 84) + next_obs[OBS_IMAGE] = torch.randn(batch_size, 3, 84, 84) + return { + ACTION: torch.randn(batch_size, action_dim), + "reward": torch.randn(batch_size), + "state": obs, + "next_state": next_obs, + "done": torch.zeros(batch_size), + "complementary_info": {}, + } + + +def _batch_iterator(**batch_kwargs): + """Infinite iterator that yields fresh batches (mirrors a real DataMixer iterator).""" + while True: + yield _make_batch(**batch_kwargs) + + +# =========================================================================== +# Registry / config tests +# =========================================================================== + + +def test_sac_algorithm_config_registered(): + """SACAlgorithmConfig should be discoverable through the registry.""" + assert "sac" in RLAlgorithmConfig.get_known_choices() + cls = RLAlgorithmConfig.get_choice_class("sac") + assert cls is SACAlgorithmConfig + + +def test_sac_algorithm_config_from_policy_config(): + """from_policy_config embeds the policy config and uses SAC defaults.""" + sac_cfg = _make_sac_config() + algo_cfg = SACAlgorithmConfig.from_policy_config(sac_cfg) + assert algo_cfg.policy_config is sac_cfg + assert algo_cfg.discrete_critic_network_kwargs is sac_cfg.discrete_critic_network_kwargs + # Defaults come from SACAlgorithmConfig, not from the policy config. + assert algo_cfg.utd_ratio == 1 + assert algo_cfg.policy_update_freq == 1 + assert algo_cfg.grad_clip_norm == 40.0 + assert algo_cfg.actor_lr == 3e-4 + + +# =========================================================================== +# TrainingStats tests +# =========================================================================== + + +def test_training_stats_defaults(): + stats = TrainingStats() + assert stats.losses == {} + assert stats.grad_norms == {} + assert stats.extra == {} + + +# =========================================================================== +# get_weights +# =========================================================================== + + +def test_get_weights_returns_policy_state_dict(): + algorithm, policy = _make_algorithm() + weights = algorithm.get_weights() + assert "policy" in weights + actor_state_dict = policy.actor.state_dict() + for key in actor_state_dict: + assert key in weights["policy"] + assert torch.equal(weights["policy"][key].cpu(), actor_state_dict[key].cpu()) + + +def test_get_weights_includes_discrete_critic_when_present(): + algorithm, _ = _make_algorithm(num_discrete_actions=3, action_dim=6) + weights = algorithm.get_weights() + assert "discrete_critic" in weights + assert len(weights["discrete_critic"]) > 0 + + +def test_get_weights_excludes_discrete_critic_when_absent(): + algorithm, _ = _make_algorithm() + weights = algorithm.get_weights() + assert "discrete_critic" not in weights + + +def test_get_weights_are_on_cpu(): + algorithm, _ = _make_algorithm(num_discrete_actions=3, action_dim=6) + weights = algorithm.get_weights() + for group_name, state_dict in weights.items(): + for key, tensor in state_dict.items(): + assert tensor.device == torch.device("cpu"), f"{group_name}/{key} is not on CPU" + + +# =========================================================================== +# select_action (lives on the policy, not the algorithm) +# =========================================================================== + + +def test_select_action_returns_correct_shape(): + action_dim = 6 + _, policy = _make_algorithm(state_dim=10, action_dim=action_dim) + policy.eval() + obs = {OBS_STATE: torch.randn(10)} + action = policy.select_action(obs) + assert action.shape == (action_dim,) + + +def test_select_action_with_discrete_critic(): + continuous_dim = 5 + _, policy = _make_algorithm(state_dim=10, action_dim=continuous_dim, num_discrete_actions=3) + policy.eval() + obs = {OBS_STATE: torch.randn(10)} + action = policy.select_action(obs) + assert action.shape == (continuous_dim + 1,) + + +# =========================================================================== +# update (single batch, utd_ratio=1) +# =========================================================================== + + +def test_update_returns_training_stats(): + algorithm, _ = _make_algorithm() + stats = algorithm.update(_batch_iterator()) + assert isinstance(stats, TrainingStats) + assert "loss_critic" in stats.losses + assert isinstance(stats.losses["loss_critic"], float) + + +def test_update_populates_actor_and_temperature_losses(): + """With policy_update_freq=1 and step 0, actor/temperature should be updated.""" + algorithm, _ = _make_algorithm(policy_update_freq=1) + stats = algorithm.update(_batch_iterator()) + assert "loss_actor" in stats.losses + assert "loss_temperature" in stats.losses + assert "temperature" in stats.extra + + +@pytest.mark.parametrize("policy_update_freq", [2, 3]) +def test_update_skips_actor_at_non_update_steps(policy_update_freq): + """Actor/temperature should only update when optimization_step % freq == 0.""" + algorithm, _ = _make_algorithm(policy_update_freq=policy_update_freq) + it = _batch_iterator() + + # Step 0: should update actor + stats_0 = algorithm.update(it) + assert "loss_actor" in stats_0.losses + + # Step 1: should NOT update actor + stats_1 = algorithm.update(it) + assert "loss_actor" not in stats_1.losses + + +def test_update_increments_optimization_step(): + algorithm, _ = _make_algorithm() + it = _batch_iterator() + assert algorithm.optimization_step == 0 + algorithm.update(it) + assert algorithm.optimization_step == 1 + algorithm.update(it) + assert algorithm.optimization_step == 2 + + +def test_update_with_discrete_critic(): + algorithm, _ = _make_algorithm(num_discrete_actions=3, action_dim=6) + stats = algorithm.update(_batch_iterator(action_dim=7)) # continuous + 1 discrete + assert "loss_discrete_critic" in stats.losses + assert "discrete_critic" in stats.grad_norms + + +# =========================================================================== +# update with UTD ratio > 1 +# =========================================================================== + + +@pytest.mark.parametrize("utd_ratio", [2, 4]) +def test_update_with_utd_ratio(utd_ratio): + algorithm, _ = _make_algorithm(utd_ratio=utd_ratio) + stats = algorithm.update(_batch_iterator()) + assert isinstance(stats, TrainingStats) + assert "loss_critic" in stats.losses + assert algorithm.optimization_step == 1 + + +def test_update_utd_ratio_pulls_utd_batches(): + """next(batch_iterator) should be called exactly utd_ratio times.""" + utd_ratio = 3 + algorithm, _ = _make_algorithm(utd_ratio=utd_ratio) + + call_count = 0 + + def counting_iterator(): + nonlocal call_count + while True: + call_count += 1 + yield _make_batch() + + algorithm.update(counting_iterator()) + assert call_count == utd_ratio + + +def test_update_utd_ratio_3_critic_warmup_changes_weights(): + """With utd_ratio=3, critic weights should change after update (3 critic steps).""" + algorithm, policy = _make_algorithm(utd_ratio=3) + + critic_params_before = {n: p.clone() for n, p in algorithm.critic_ensemble.named_parameters()} + + algorithm.update(_batch_iterator()) + + changed = False + for n, p in algorithm.critic_ensemble.named_parameters(): + if not torch.equal(p, critic_params_before[n]): + changed = True + break + assert changed, "Critic weights should have changed after UTD update" + + +# =========================================================================== +# get_observation_features +# =========================================================================== + + +def test_get_observation_features_returns_none_without_frozen_encoder(): + algorithm, _ = _make_algorithm(with_images=False) + obs = {OBS_STATE: torch.randn(4, 10)} + next_obs = {OBS_STATE: torch.randn(4, 10)} + feat, next_feat = algorithm.get_observation_features(obs, next_obs) + assert feat is None + assert next_feat is None + + +# =========================================================================== +# optimization_step setter +# =========================================================================== + + +def test_optimization_step_can_be_set_for_resume(): + algorithm, _ = _make_algorithm() + algorithm.optimization_step = 100 + assert algorithm.optimization_step == 100 + + +# =========================================================================== +# make_algorithm factory +# =========================================================================== + + +def test_make_algorithm_returns_sac_for_sac_policy(): + sac_cfg = _make_sac_config() + policy = GaussianActorPolicy(config=sac_cfg) + algorithm = make_algorithm(cfg=SACAlgorithmConfig.from_policy_config(sac_cfg), policy=policy) + assert isinstance(algorithm, SACAlgorithm) + assert algorithm.optimizers == {} + + +def test_make_optimizers_creates_expected_keys(): + """make_optimizers_and_scheduler() should populate the algorithm with Adam optimizers.""" + sac_cfg = _make_sac_config() + policy = GaussianActorPolicy(config=sac_cfg) + algorithm = make_algorithm(cfg=SACAlgorithmConfig.from_policy_config(sac_cfg), policy=policy) + optimizers = algorithm.make_optimizers_and_scheduler() + assert "actor" in optimizers + assert "critic" in optimizers + assert "temperature" in optimizers + assert all(isinstance(v, torch.optim.Adam) for v in optimizers.values()) + assert algorithm.get_optimizers() is optimizers + + +def test_actor_side_no_optimizers(): + """Actor-side usage: no optimizers needed, make_optimizers_and_scheduler is not called.""" + sac_cfg = _make_sac_config() + policy = GaussianActorPolicy(config=sac_cfg) + algorithm = make_algorithm(cfg=SACAlgorithmConfig.from_policy_config(sac_cfg), policy=policy) + assert isinstance(algorithm, SACAlgorithm) + assert algorithm.optimizers == {} + + +def test_make_algorithm_uses_sac_algorithm_defaults(): + """make_algorithm populates SACAlgorithmConfig with its own defaults.""" + sac_cfg = _make_sac_config() + policy = GaussianActorPolicy(config=sac_cfg) + algorithm = make_algorithm(cfg=SACAlgorithmConfig.from_policy_config(sac_cfg), policy=policy) + assert algorithm.config.utd_ratio == 1 + assert algorithm.config.policy_update_freq == 1 + assert algorithm.config.grad_clip_norm == 40.0 + + +def test_unknown_algorithm_name_raises_in_registry(): + """The ChoiceRegistry is the source of truth for unknown algorithm names.""" + with pytest.raises(KeyError): + RLAlgorithmConfig.get_choice_class("unknown_algo") + + +# =========================================================================== +# load_weights (round-trip with get_weights) +# =========================================================================== + + +def test_load_weights_round_trip(): + """get_weights -> load_weights should restore identical parameters on a fresh policy.""" + algo_src, _ = _make_algorithm(state_dim=10, action_dim=6) + algo_src.update(_batch_iterator()) + + sac_cfg = _make_sac_config(state_dim=10, action_dim=6) + policy_dst = GaussianActorPolicy(config=sac_cfg) + algo_dst = SACAlgorithm(policy=policy_dst, config=algo_src.config) + + weights = algo_src.get_weights() + algo_dst.load_weights(weights, device="cpu") + + dst_actor_state_dict = algo_dst.policy.actor.state_dict() + for key, tensor in weights["policy"].items(): + assert torch.equal( + dst_actor_state_dict[key].cpu(), + tensor.cpu(), + ), f"Policy param '{key}' mismatch after load_weights" + + +def test_load_weights_round_trip_with_discrete_critic(): + algo_src, _ = _make_algorithm(num_discrete_actions=3, action_dim=6) + algo_src.update(_batch_iterator(action_dim=7)) + + sac_cfg = _make_sac_config(num_discrete_actions=3, action_dim=6) + policy_dst = GaussianActorPolicy(config=sac_cfg) + algo_dst = SACAlgorithm(policy=policy_dst, config=algo_src.config) + + weights = algo_src.get_weights() + algo_dst.load_weights(weights, device="cpu") + + assert "discrete_critic" in weights + assert len(weights["discrete_critic"]) > 0 + dst_discrete_critic_state_dict = algo_dst.policy.discrete_critic.state_dict() + for key, tensor in weights["discrete_critic"].items(): + assert torch.equal( + dst_discrete_critic_state_dict[key].cpu(), + tensor.cpu(), + ), f"Discrete critic param '{key}' mismatch after load_weights" + + +def test_load_weights_ignores_missing_discrete_critic(): + """load_weights should not fail when weights lack discrete_critic on a non-discrete policy.""" + algorithm, _ = _make_algorithm() + weights = algorithm.get_weights() + algorithm.load_weights(weights, device="cpu") + + +def test_actor_side_weight_sync_with_discrete_critic(): + """End-to-end: learner ``algorithm.get_weights()`` -> actor ``algorithm.load_weights()``.""" + # Learner side: train the source algorithm so its weights diverge from init. + algo_src, _ = _make_algorithm(num_discrete_actions=3, action_dim=6) + algo_src.update(_batch_iterator(action_dim=7)) + weights = algo_src.get_weights() + + # Actor side: fresh policy + fresh algorithm holding it. + sac_cfg = _make_sac_config(num_discrete_actions=3, action_dim=6) + policy_actor = GaussianActorPolicy(config=sac_cfg) + algo_actor = SACAlgorithm( + policy=policy_actor, + config=SACAlgorithmConfig.from_policy_config(sac_cfg), + ) + + # Snapshot initial actor state for the "did it change?" assertion below. + initial_discrete_critic_state_dict = { + k: v.clone() for k, v in policy_actor.discrete_critic.state_dict().items() + } + + algo_actor.load_weights(weights, device="cpu") + + # Actor weights match the learner's exported actor state dict. + actor_state_dict = policy_actor.actor.state_dict() + for key, tensor in weights["policy"].items(): + assert torch.equal(actor_state_dict[key].cpu(), tensor.cpu()), ( + f"Actor param '{key}' not synced by algorithm.load_weights" + ) + + # Discrete critic weights match the learner's exported discrete critic. + discrete_critic_state_dict = policy_actor.discrete_critic.state_dict() + for key, tensor in weights["discrete_critic"].items(): + assert torch.equal(discrete_critic_state_dict[key].cpu(), tensor.cpu()), ( + f"Discrete critic param '{key}' not synced by algorithm.load_weights" + ) + + # Sanity: the discrete critic actually changed (otherwise the sync is trivial). + changed = any( + not torch.equal(initial_discrete_critic_state_dict[key], discrete_critic_state_dict[key]) + for key in initial_discrete_critic_state_dict + if key in discrete_critic_state_dict + ) + assert changed, "Discrete critic weights did not change between init and after sync" + + +# =========================================================================== +# TrainingStats generic losses dict +# =========================================================================== + + +def test_training_stats_generic_losses(): + stats = TrainingStats( + losses={"loss_bc": 0.5, "loss_q": 1.2}, + extra={"temperature": 0.1}, + ) + assert stats.losses["loss_bc"] == 0.5 + assert stats.losses["loss_q"] == 1.2 + assert stats.extra["temperature"] == 0.1 + + +# =========================================================================== +# Registry-driven make_algorithm +# =========================================================================== + + +def test_make_algorithm_builds_sac(): + """make_algorithm should look up the SAC class from the registry and instantiate it.""" + sac_cfg = _make_sac_config() + algo_config = SACAlgorithmConfig.from_policy_config(sac_cfg) + algo_config.utd_ratio = 2 + policy = GaussianActorPolicy(config=sac_cfg) + + algorithm = make_algorithm(cfg=algo_config, policy=policy) + assert isinstance(algorithm, SACAlgorithm) + assert algorithm.config.utd_ratio == 2 + + +# =========================================================================== +# state_dict / load_state_dict (algorithm-side resume) +# =========================================================================== + + +def test_state_dict_contains_algorithm_owned_tensors(): + """state_dict should pack critics, target networks, and log_alpha (no encoder bloat).""" + algorithm, _ = _make_algorithm() + sd = algorithm.state_dict() + + assert "log_alpha" in sd + assert any(k.startswith("critic_ensemble.") for k in sd) + assert any(k.startswith("critic_target.") for k in sd) + # encoder weights live on the policy and must not be duplicated here. + assert not any(".encoder." in k for k in sd) + + +def test_state_dict_includes_discrete_critic_target_when_present(): + algorithm, _ = _make_algorithm(num_discrete_actions=3, action_dim=6) + sd = algorithm.state_dict() + assert any(k.startswith("discrete_critic_target.") for k in sd) + + +def test_load_state_dict_round_trip_restores_critics_and_log_alpha(): + """state_dict -> load_state_dict on a fresh algorithm restores all bytes exactly.""" + sac_cfg = _make_sac_config(num_discrete_actions=3, action_dim=6) + src_policy = GaussianActorPolicy(config=sac_cfg) + src = SACAlgorithm(policy=src_policy, config=SACAlgorithmConfig.from_policy_config(sac_cfg)) + src.make_optimizers_and_scheduler() + # Train a few steps so weights diverge from init (action_dim=7 = 6 continuous + 1 discrete). + src.update(_batch_iterator(action_dim=7)) + src.update(_batch_iterator(action_dim=7)) + + dst_policy = GaussianActorPolicy(config=sac_cfg) + dst = SACAlgorithm(policy=dst_policy, config=SACAlgorithmConfig.from_policy_config(sac_cfg)) + dst.make_optimizers_and_scheduler() + + src_sd = src.state_dict() + dst.load_state_dict(src_sd) + dst_sd = dst.state_dict() + + assert set(dst_sd) == set(src_sd) + for key in src_sd: + assert torch.allclose(src_sd[key].cpu(), dst_sd[key].cpu()), f"{key} mismatch after round-trip" + + +def test_load_state_dict_preserves_log_alpha_parameter_identity(): + """The temperature optimizer holds a reference to log_alpha; identity must survive load.""" + algorithm, _ = _make_algorithm() + log_alpha_id_before = id(algorithm.log_alpha) + optimizer_param_id = id(algorithm.optimizers["temperature"].param_groups[0]["params"][0]) + assert log_alpha_id_before == optimizer_param_id + + new_state = algorithm.state_dict() + new_state["log_alpha"] = torch.tensor([0.42]) + algorithm.load_state_dict(new_state) + + assert id(algorithm.log_alpha) == log_alpha_id_before + assert id(algorithm.optimizers["temperature"].param_groups[0]["params"][0]) == log_alpha_id_before + assert torch.allclose(algorithm.log_alpha.detach().cpu(), torch.tensor([0.42])) + + +def test_save_pretrained_round_trip_via_disk(tmp_path): + """End-to-end: save_pretrained -> from_pretrained restores tensors and config.""" + sac_cfg = _make_sac_config() + src_policy = GaussianActorPolicy(config=sac_cfg) + src = SACAlgorithm(policy=src_policy, config=SACAlgorithmConfig.from_policy_config(sac_cfg)) + src.make_optimizers_and_scheduler() + src.update(_batch_iterator()) + + save_dir = tmp_path / "algorithm" + src.save_pretrained(save_dir) + assert (save_dir / "model.safetensors").is_file() + assert (save_dir / "config.json").is_file() + + dst_policy = GaussianActorPolicy(config=sac_cfg) + dst = SACAlgorithm.from_pretrained(save_dir, policy=dst_policy) + + src_sd = src.state_dict() + dst_sd = dst.state_dict() + assert set(src_sd) == set(dst_sd) + for key in src_sd: + assert torch.allclose(src_sd[key].cpu(), dst_sd[key].cpu()), f"{key} mismatch after disk round-trip" diff --git a/tests/rl/test_trainer.py b/tests/rl/test_trainer.py new file mode 100644 index 000000000..b15d4393b --- /dev/null +++ b/tests/rl/test_trainer.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python + +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])") + +import torch # noqa: E402 +from torch import Tensor # noqa: E402 + +from lerobot.rl.algorithms.base import RLAlgorithm # noqa: E402 +from lerobot.rl.algorithms.configs import TrainingStats # noqa: E402 +from lerobot.rl.trainer import RLTrainer # noqa: E402 +from lerobot.utils.constants import ACTION, OBS_STATE # noqa: E402 + + +class _DummyRLAlgorithmConfig: + """Dummy config for testing.""" + + +class _DummyRLAlgorithm(RLAlgorithm): + config_class = _DummyRLAlgorithmConfig + name = "dummy_rl_algorithm" + + def __init__(self): + self.configure_calls = 0 + self.update_calls = 0 + + def select_action(self, observation: dict[str, Tensor]) -> Tensor: + return torch.zeros(1) + + def configure_data_iterator( + self, + data_mixer, + batch_size: int, + *, + async_prefetch: bool = True, + queue_size: int = 2, + ): + self.configure_calls += 1 + return data_mixer.get_iterator( + batch_size=batch_size, + async_prefetch=async_prefetch, + queue_size=queue_size, + ) + + def make_optimizers_and_scheduler(self): + return {} + + def update(self, batch_iterator): + self.update_calls += 1 + _ = next(batch_iterator) + return TrainingStats(losses={"dummy": 1.0}) + + def load_weights(self, weights, device="cpu") -> None: + _ = (weights, device) + + def state_dict(self) -> dict[str, torch.Tensor]: + return {} + + def load_state_dict(self, state_dict, device="cpu") -> None: + _ = (state_dict, device) + + +class _SimpleMixer: + def get_iterator(self, batch_size: int, async_prefetch: bool = True, queue_size: int = 2): + _ = (async_prefetch, queue_size) + while True: + yield { + "state": {OBS_STATE: torch.randn(batch_size, 3)}, + ACTION: torch.randn(batch_size, 2), + "reward": torch.randn(batch_size), + "next_state": {OBS_STATE: torch.randn(batch_size, 3)}, + "done": torch.zeros(batch_size), + "truncated": torch.zeros(batch_size), + "complementary_info": None, + } + + +def test_trainer_lazy_iterator_lifecycle_and_reset(): + algo = _DummyRLAlgorithm() + mixer = _SimpleMixer() + trainer = RLTrainer(algorithm=algo, data_mixer=mixer, batch_size=4) + + # First call builds iterator once. + trainer.training_step() + assert algo.configure_calls == 1 + assert algo.update_calls == 1 + + # Second call reuses existing iterator. + trainer.training_step() + assert algo.configure_calls == 1 + assert algo.update_calls == 2 + + # Explicit reset forces lazy rebuild on next step. + trainer.reset_data_iterator() + trainer.training_step() + assert algo.configure_calls == 2 + assert algo.update_calls == 3 + + +def test_trainer_set_data_mixer_resets_by_default(): + algo = _DummyRLAlgorithm() + mixer_a = _SimpleMixer() + mixer_b = _SimpleMixer() + trainer = RLTrainer(algorithm=algo, data_mixer=mixer_a, batch_size=2) + + trainer.training_step() + assert algo.configure_calls == 1 + + trainer.set_data_mixer(mixer_b, reset=True) + trainer.training_step() + assert algo.configure_calls == 2 + + +def test_algorithm_optimization_step_contract_defaults(): + algo = _DummyRLAlgorithm() + assert algo.optimization_step == 0 + algo.optimization_step = 11 + assert algo.optimization_step == 11 diff --git a/tests/utils/test_process.py b/tests/utils/test_process.py index 65b24aac4..1ede0bfeb 100644 --- a/tests/utils/test_process.py +++ b/tests/utils/test_process.py @@ -22,7 +22,7 @@ from unittest.mock import patch import pytest -pytest.importorskip("grpc") +pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])") from lerobot.utils.process import ProcessSignalHandler # noqa: E402 diff --git a/tests/utils/test_replay_buffer.py b/tests/utils/test_replay_buffer.py index 1b2af39f1..e6517596f 100644 --- a/tests/utils/test_replay_buffer.py +++ b/tests/utils/test_replay_buffer.py @@ -19,7 +19,6 @@ from collections.abc import Callable import pytest -pytest.importorskip("grpc") pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])") import torch # noqa: E402 diff --git a/uv.lock b/uv.lock index f09c4a64f..28b906d89 100644 --- a/uv.lock +++ b/uv.lock @@ -2849,10 +2849,16 @@ hardware = [ { name = "pyserial" }, ] hilserl = [ + { name = "av" }, + { name = "datasets" }, { name = "grpcio" }, { name = "gym-hil" }, + { name = "jsonlines" }, + { name = "pandas" }, { name = "placo" }, { name = "protobuf" }, + { name = "pyarrow" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, { name = "transformers" }, ] hopejr = [ @@ -3069,6 +3075,7 @@ requires-dist = [ { name = "lerobot", extras = ["dataset"], marker = "extra == 'aloha'" }, { name = "lerobot", extras = ["dataset"], marker = "extra == 'core-scripts'" }, { name = "lerobot", extras = ["dataset"], marker = "extra == 'dataset-viz'" }, + { name = "lerobot", extras = ["dataset"], marker = "extra == 'hilserl'" }, { name = "lerobot", extras = ["dataset"], marker = "extra == 'libero'" }, { name = "lerobot", extras = ["dataset"], marker = "extra == 'metaworld'" }, { name = "lerobot", extras = ["dataset"], marker = "extra == 'pusht'" }, From 04125492e47ec264f1147676aa0d17347ddfaa08 Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Tue, 12 May 2026 16:59:11 +0200 Subject: [PATCH 10/18] fix(datasets): expand torchcodec platform coverage + rewrite pyav fallback for torchvision >0.26 (#3588) * fix(deps): better versioning control for torchcodec * refactor(video_utils): replace torchvision with pyav * adding Torchcodec version to lerobot-info * chore(benchmarks): delete video benchmark --------- Co-authored-by: Maximellerbach --- benchmarks/video/README.md | 288 -------------- benchmarks/video/run_video_benchmark.py | 488 ------------------------ pyproject.toml | 13 +- src/lerobot/datasets/video_utils.py | 113 +++--- src/lerobot/scripts/lerobot_info.py | 1 + uv.lock | 64 ++-- 6 files changed, 112 insertions(+), 855 deletions(-) delete mode 100644 benchmarks/video/README.md delete mode 100644 benchmarks/video/run_video_benchmark.py diff --git a/benchmarks/video/README.md b/benchmarks/video/README.md deleted file mode 100644 index 1feee69c4..000000000 --- a/benchmarks/video/README.md +++ /dev/null @@ -1,288 +0,0 @@ -# Video benchmark - -## Questions - -What is the optimal trade-off between: - -- maximizing loading time with random access, -- minimizing memory space on disk, -- maximizing success rate of policies, -- compatibility across devices/platforms for decoding videos (e.g. video players, web browsers). - -How to encode videos? - -- Which video codec (`-vcodec`) to use? h264, h265, AV1? -- What pixel format to use (`-pix_fmt`)? `yuv444p` or `yuv420p`? -- How much compression (`-crf`)? No compression with `0`, intermediate compression with `25` or extreme with `50+`? -- Which frequency to chose for key frames (`-g`)? A key frame every `10` frames? - -How to decode videos? - -- Which `decoder`? `torchvision`, `torchaudio`, `ffmpegio`, `decord`, or `nvc`? -- What scenarios to use for the requesting timestamps during benchmark? (`timestamps_mode`) - -## Variables - -**Image content & size** -We don't expect the same optimal settings for a dataset of images from a simulation, or from real-world in an apartment, or in a factory, or outdoor, or with lots of moving objects in the scene, etc. Similarly, loading times might not vary linearly with the image size (resolution). -For these reasons, we run this benchmark on four representative datasets: - -- `lerobot/pusht_image`: (96 x 96 pixels) simulation with simple geometric shapes, fixed camera. -- `lerobot/aloha_mobile_shrimp_image`: (480 x 640 pixels) real-world indoor, moving camera. -- `lerobot/paris_street`: (720 x 1280 pixels) real-world outdoor, moving camera. -- `lerobot/kitchen`: (1080 x 1920 pixels) real-world indoor, fixed camera. - -Note: The datasets used for this benchmark need to be image datasets, not video datasets. - -**Data augmentations** -We might revisit this benchmark and find better settings if we train our policies with various data augmentations to make them more robust (e.g. robust to color changes, compression, etc.). - -### Encoding parameters - -| parameter | values | -| ----------- | ------------------------------------------------------------ | -| **vcodec** | `libx264`, `libx265`, `libsvtav1` | -| **pix_fmt** | `yuv444p`, `yuv420p` | -| **g** | `1`, `2`, `3`, `4`, `5`, `6`, `10`, `15`, `20`, `40`, `None` | -| **crf** | `0`, `5`, `10`, `15`, `20`, `25`, `30`, `40`, `50`, `None` | - -Note that `crf` value might be interpreted differently by various video codecs. In other words, the same value used with one codec doesn't necessarily translate into the same compression level with another codec. In fact, the default value (`None`) isn't the same amongst the different video codecs. Importantly, it is also the case for many other ffmpeg arguments like `g` which specifies the frequency of the key frames. - -For a comprehensive list and documentation of these parameters, see the ffmpeg documentation depending on the video codec used: - -- h264: https://trac.ffmpeg.org/wiki/Encode/H.264 -- h265: https://trac.ffmpeg.org/wiki/Encode/H.265 -- AV1: https://trac.ffmpeg.org/wiki/Encode/AV1 - -### Decoding parameters - -**Decoder** -We tested two video decoding backends from torchvision: - -- `pyav` -- `video_reader` (requires to build torchvision from source) - -**Requested timestamps** -Given the way video decoding works, once a keyframe has been loaded, the decoding of subsequent frames is fast. -This of course is affected by the `-g` parameter during encoding, which specifies the frequency of the keyframes. Given our typical use cases in robotics policies which might request a few timestamps in different random places, we want to replicate these use cases with the following scenarios: - -- `1_frame`: 1 frame, -- `2_frames`: 2 consecutive frames (e.g. `[t, t + 1 / fps]`), -- `6_frames`: 6 consecutive frames (e.g. `[t + i / fps for i in range(6)]`) - -Note that this differs significantly from a typical use case like watching a movie, in which every frame is loaded sequentially from the beginning to the end and it's acceptable to have big values for `-g`. - -Additionally, because some policies might request single timestamps that are a few frames apart, we also have the following scenario: - -- `2_frames_4_space`: 2 frames with 4 consecutive frames of spacing in between (e.g `[t, t + 5 / fps]`), - -However, due to how video decoding is implemented with `pyav`, we don't have access to an accurate seek so in practice this scenario is essentially the same as `6_frames` since all 6 frames between `t` and `t + 5 / fps` will be decoded. - -## Metrics - -**Data compression ratio (lower is better)** -`video_images_size_ratio` is the ratio of the memory space on disk taken by the encoded video over the memory space taken by the original images. For instance, `video_images_size_ratio=25%` means that the video takes 4 times less memory space on disk compared to the original images. - -**Loading time ratio (lower is better)** -`video_images_load_time_ratio` is the ratio of the time it takes to decode frames from the video at a given timestamps over the time it takes to load the exact same original images. Lower is better. For instance, `video_images_load_time_ratio=200%` means that decoding from video is 2 times slower than loading the original images. - -**Average Mean Square Error (lower is better)** -`avg_mse` is the average mean square error between each decoded frame and its corresponding original image over all requested timestamps, and also divided by the number of pixels in the image to be comparable when switching to different image sizes. - -**Average Peak Signal to Noise Ratio (higher is better)** -`avg_psnr` measures the ratio between the maximum possible power of a signal and the power of corrupting noise that affects the fidelity of its representation. Higher PSNR indicates better quality. - -**Average Structural Similarity Index Measure (higher is better)** -`avg_ssim` evaluates the perceived quality of images by comparing luminance, contrast, and structure. SSIM values range from -1 to 1, where 1 indicates perfect similarity. - -One aspect that can't be measured here with those metrics is the compatibility of the encoding across platforms, in particular on web browser, for visualization purposes. -h264, h265 and AV1 are all commonly used codecs and should not pose an issue. However, the chroma subsampling (`pix_fmt`) format might affect compatibility: - -- `yuv420p` is more widely supported across various platforms, including web browsers. -- `yuv444p` offers higher color fidelity but might not be supported as broadly. - - - -## How the benchmark works - -The benchmark evaluates both encoding and decoding of video frames on the first episode of each dataset. - -**Encoding:** for each `vcodec` and `pix_fmt` pair, we use a default value for `g` and `crf` upon which we change a single value (either `g` or `crf`) to one of the specified values (we don't test every combination of those as this would be computationally too heavy). -This gives a unique set of encoding parameters which is used to encode the episode. - -**Decoding:** Then, for each of those unique encodings, we iterate through every combination of the decoding parameters `backend` and `timestamps_mode`. For each of them, we record the metrics of a number of samples (given by `--num-samples`). This is parallelized for efficiency and the number of processes can be controlled with `--num-workers`. Ideally, it's best to have a `--num-samples` that is divisible by `--num-workers`. - -Intermediate results saved for each `vcodec` and `pix_fmt` combination in csv tables. -These are then all concatenated to a single table ready for analysis. - -## Caveats - -We tried to measure the most impactful parameters for both encoding and decoding. However, for computational reasons we can't test out every combination. - -Additional encoding parameters exist that are not included in this benchmark. In particular: - -- `-preset` which allows for selecting encoding presets. This represents a collection of options that will provide a certain encoding speed to compression ratio. By leaving this parameter unspecified, it is considered to be `medium` for libx264 and libx265 and `8` for libsvtav1. -- `-tune` which allows to optimize the encoding for certain aspects (e.g. film quality, fast decoding, etc.). - -See the documentation mentioned above for more detailed info on these settings and for a more comprehensive list of other parameters. - -Similarly on the decoding side, other decoders exist but are not implemented in our current benchmark. To name a few: - -- `torchaudio` -- `ffmpegio` -- `decord` -- `nvc` - -Note as well that since we are mostly interested in the performance at decoding time (also because encoding is done only once before uploading a dataset), we did not measure encoding times nor have any metrics regarding encoding. -However, besides the necessity to build ffmpeg from source, encoding did not pose any issue and it didn't take a significant amount of time during this benchmark. - -## Install - -Building ffmpeg from source is required to include libx265 and libaom/libsvtav1 (av1) video codecs ([compilation guide](https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu)). - -**Note:** While you still need to build torchvision with a conda-installed `ffmpeg<4.3` to use the `video_reader` decoder (as described in [#220](https://github.com/huggingface/lerobot/pull/220)), you also need another version which is custom-built with all the video codecs for encoding. For the script to then use that version, you can prepend the command above with `PATH="$HOME/bin:$PATH"`, which is where ffmpeg should be built. - -## Adding a video decoder - -Right now, we're only benchmarking the two video decoder available with torchvision: `pyav` and `video_reader`. -You can easily add a new decoder to benchmark by adding it to this function in the script: - -```diff -def decode_video_frames( - video_path: str, - timestamps: list[float], - tolerance_s: float, - backend: str, -) -> torch.Tensor: - if backend in ["pyav", "video_reader"]: - return decode_video_frames_torchvision( - video_path, timestamps, tolerance_s, backend - ) -+ elif backend == ["your_decoder"]: -+ return your_decoder_function( -+ video_path, timestamps, tolerance_s, backend -+ ) - else: - raise NotImplementedError(backend) -``` - -## Example - -For a quick run, you can try these parameters: - -```bash -python benchmark/video/run_video_benchmark.py \ - --output-dir outputs/video_benchmark \ - --repo-ids \ - lerobot/pusht_image \ - lerobot/aloha_mobile_shrimp_image \ - --vcodec libx264 libx265 \ - --pix-fmt yuv444p yuv420p \ - --g 2 20 None \ - --crf 10 40 None \ - --timestamps-modes 1_frame 2_frames \ - --backends pyav video_reader \ - --num-samples 5 \ - --num-workers 5 \ - --save-frames 0 -``` - -## Results - -### Reproduce - -We ran the benchmark with the following parameters: - -```bash -# h264 and h265 encodings -python benchmark/video/run_video_benchmark.py \ - --output-dir outputs/video_benchmark \ - --repo-ids \ - lerobot/pusht_image \ - lerobot/aloha_mobile_shrimp_image \ - lerobot/paris_street \ - lerobot/kitchen \ - --vcodec libx264 libx265 \ - --pix-fmt yuv444p yuv420p \ - --g 1 2 3 4 5 6 10 15 20 40 None \ - --crf 0 5 10 15 20 25 30 40 50 None \ - --timestamps-modes 1_frame 2_frames 6_frames \ - --backends pyav video_reader \ - --num-samples 50 \ - --num-workers 5 \ - --save-frames 1 - -# av1 encoding (only compatible with yuv420p and pyav decoder) -python benchmark/video/run_video_benchmark.py \ - --output-dir outputs/video_benchmark \ - --repo-ids \ - lerobot/pusht_image \ - lerobot/aloha_mobile_shrimp_image \ - lerobot/paris_street \ - lerobot/kitchen \ - --vcodec libsvtav1 \ - --pix-fmt yuv420p \ - --g 1 2 3 4 5 6 10 15 20 40 None \ - --crf 0 5 10 15 20 25 30 40 50 None \ - --timestamps-modes 1_frame 2_frames 6_frames \ - --backends pyav \ - --num-samples 50 \ - --num-workers 5 \ - --save-frames 1 -``` - -The full results are available [here](https://docs.google.com/spreadsheets/d/1OYJB43Qu8fC26k_OyoMFgGBBKfQRCi4BIuYitQnq3sw/edit?usp=sharing) - -### Parameters selected for LeRobotDataset - -Considering these results, we chose what we think is the best set of encoding parameter: - -- vcodec: `libsvtav1` -- pix-fmt: `yuv420p` -- g: `2` -- crf: `30` - -Since we're using av1 encoding, we're choosing the `pyav` decoder as `video_reader` does not support it (and `pyav` doesn't require a custom build of `torchvision`). - -### Summary - -These tables show the results for `g=2` and `crf=30`, using `timestamps-modes=6_frames` and `backend=pyav` - -| video_images_size_ratio | vcodec | pix_fmt | | | | -| --------------------------------- | ---------- | ------- | --------- | --------- | --------- | -| | libx264 | | libx265 | | libsvtav1 | -| repo_id | yuv420p | yuv444p | yuv420p | yuv444p | yuv420p | -| lerobot/pusht_image | **16.97%** | 17.58% | 18.57% | 18.86% | 22.06% | -| lerobot/aloha_mobile_shrimp_image | 2.14% | 2.11% | 1.38% | **1.37%** | 5.59% | -| lerobot/paris_street | 2.12% | 2.13% | **1.54%** | **1.54%** | 4.43% | -| lerobot/kitchen | 1.40% | 1.39% | **1.00%** | **1.00%** | 2.52% | - -| video_images_load_time_ratio | vcodec | pix_fmt | | | | -| --------------------------------- | ------- | ------- | -------- | ------- | --------- | -| | libx264 | | libx265 | | libsvtav1 | -| repo_id | yuv420p | yuv444p | yuv420p | yuv444p | yuv420p | -| lerobot/pusht_image | 6.45 | 5.19 | **1.90** | 2.12 | 2.47 | -| lerobot/aloha_mobile_shrimp_image | 11.80 | 7.92 | 0.71 | 0.85 | **0.48** | -| lerobot/paris_street | 2.21 | 2.05 | 0.36 | 0.49 | **0.30** | -| lerobot/kitchen | 1.46 | 1.46 | 0.28 | 0.51 | **0.26** | - -| | | vcodec | pix_fmt | | | | -| --------------------------------- | -------- | -------- | ------------ | -------- | --------- | ------------ | -| | | libx264 | | libx265 | | libsvtav1 | -| repo_id | metric | yuv420p | yuv444p | yuv420p | yuv444p | yuv420p | -| lerobot/pusht_image | avg_mse | 2.90E-04 | **2.03E-04** | 3.13E-04 | 2.29E-04 | 2.19E-04 | -| | avg_psnr | 35.44 | 37.07 | 35.49 | **37.30** | 37.20 | -| | avg_ssim | 98.28% | **98.85%** | 98.31% | 98.84% | 98.72% | -| lerobot/aloha_mobile_shrimp_image | avg_mse | 2.76E-04 | 2.59E-04 | 3.17E-04 | 3.06E-04 | **1.30E-04** | -| | avg_psnr | 35.91 | 36.21 | 35.88 | 36.09 | **40.17** | -| | avg_ssim | 95.19% | 95.18% | 95.00% | 95.05% | **97.73%** | -| lerobot/paris_street | avg_mse | 6.89E-04 | 6.70E-04 | 4.03E-03 | 4.02E-03 | **3.09E-04** | -| | avg_psnr | 33.48 | 33.68 | 32.05 | 32.15 | **35.40** | -| | avg_ssim | 93.76% | 93.75% | 89.46% | 89.46% | **95.46%** | -| lerobot/kitchen | avg_mse | 2.50E-04 | 2.24E-04 | 4.28E-04 | 4.18E-04 | **1.53E-04** | -| | avg_psnr | 36.73 | 37.33 | 36.56 | 36.75 | **39.12** | -| | avg_ssim | 95.47% | 95.58% | 95.52% | 95.53% | **96.82%** | diff --git a/benchmarks/video/run_video_benchmark.py b/benchmarks/video/run_video_benchmark.py deleted file mode 100644 index 064a84b48..000000000 --- a/benchmarks/video/run_video_benchmark.py +++ /dev/null @@ -1,488 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2024 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Assess the performance of video decoding in various configurations. - -This script will benchmark different video encoding and decoding parameters. -See the provided README.md or run `python benchmark/video/run_video_benchmark.py --help` for usage info. -""" - -import argparse -import datetime as dt -import itertools -import random -import shutil -from collections import OrderedDict -from concurrent.futures import ThreadPoolExecutor, as_completed -from pathlib import Path -from threading import Lock - -import einops -import numpy as np -import pandas as pd -import PIL -import torch -from skimage.metrics import mean_squared_error, peak_signal_noise_ratio, structural_similarity -from tqdm import tqdm - -from lerobot.datasets.lerobot_dataset import LeRobotDataset -from lerobot.datasets.video_utils import ( - decode_video_frames, - encode_video_frames, -) -from lerobot.utils.constants import OBS_IMAGE -from lerobot.utils.utils import TimerManager - -BASE_ENCODING = OrderedDict( - [ - ("vcodec", "libx264"), - ("pix_fmt", "yuv444p"), - ("g", 2), - ("crf", None), - # TODO(aliberts): Add fastdecode - # ("fastdecode", 0), - ] -) - - -# TODO(rcadene, aliberts): move to `utils.py` folder when we want to refactor -def parse_int_or_none(value) -> int | None: - if value.lower() == "none": - return None - try: - return int(value) - except ValueError as e: - raise argparse.ArgumentTypeError(f"Invalid int or None: {value}") from e - - -def check_datasets_formats(repo_ids: list) -> None: - for repo_id in repo_ids: - dataset = LeRobotDataset(repo_id) - if len(dataset.meta.video_keys) > 0: - raise ValueError( - f"Use only image dataset for running this benchmark. Video dataset provided: {repo_id}" - ) - - -def get_directory_size(directory: Path) -> int: - total_size = 0 - for item in directory.rglob("*"): - if item.is_file(): - total_size += item.stat().st_size - return total_size - - -def load_original_frames(imgs_dir: Path, timestamps: list[float], fps: int) -> torch.Tensor: - frames = [] - for ts in timestamps: - idx = int(ts * fps) - frame = PIL.Image.open(imgs_dir / f"frame-{idx:06d}.png") - frame = torch.from_numpy(np.array(frame)) - frame = frame.type(torch.float32) / 255 - frame = einops.rearrange(frame, "h w c -> c h w") - frames.append(frame) - return torch.stack(frames) - - -def save_decoded_frames( - imgs_dir: Path, save_dir: Path, frames: torch.Tensor, timestamps: list[float], fps: int -) -> None: - if save_dir.exists() and len(list(save_dir.glob("frame-*.png"))) == len(timestamps): - return - - save_dir.mkdir(parents=True, exist_ok=True) - for i, ts in enumerate(timestamps): - idx = int(ts * fps) - frame_hwc = (frames[i].permute((1, 2, 0)) * 255).type(torch.uint8).cpu().numpy() - PIL.Image.fromarray(frame_hwc).save(save_dir / f"frame-{idx:06d}_decoded.png") - shutil.copyfile(imgs_dir / f"frame-{idx:06d}.png", save_dir / f"frame-{idx:06d}_original.png") - - -def save_first_episode(imgs_dir: Path, dataset: LeRobotDataset) -> None: - episode_index = 0 - ep_num_images = dataset.meta.episodes["length"][episode_index] - if imgs_dir.exists() and len(list(imgs_dir.glob("frame-*.png"))) == ep_num_images: - return - - imgs_dir.mkdir(parents=True, exist_ok=True) - hf_dataset = dataset.hf_dataset.with_format(None) - - # We only save images from the first camera - img_keys = [key for key in hf_dataset.features if key.startswith(OBS_IMAGE)] - imgs_dataset = hf_dataset.select_columns(img_keys[0]) - - for i, item in enumerate( - tqdm(imgs_dataset, desc=f"saving {dataset.repo_id} first episode images", leave=False) - ): - img = item[img_keys[0]] - img.save(str(imgs_dir / f"frame-{i:06d}.png"), quality=100) - - if i >= ep_num_images - 1: - break - - -def sample_timestamps(timestamps_mode: str, ep_num_images: int, fps: int) -> list[float]: - # Start at 5 to allow for 2_frames_4_space and 6_frames - idx = random.randint(5, ep_num_images - 1) - match timestamps_mode: - case "1_frame": - frame_indexes = [idx] - case "2_frames": - frame_indexes = [idx - 1, idx] - case "2_frames_4_space": - frame_indexes = [idx - 5, idx] - case "6_frames": - frame_indexes = [idx - i for i in range(6)][::-1] - case _: - raise ValueError(timestamps_mode) - - return [idx / fps for idx in frame_indexes] - - -def benchmark_decoding( - imgs_dir: Path, - video_path: Path, - timestamps_mode: str, - backend: str, - ep_num_images: int, - fps: int, - num_samples: int = 50, - num_workers: int = 4, - save_frames: bool = False, -) -> dict: - def process_sample(sample: int, lock: Lock): - time_benchmark = TimerManager(log=False) - timestamps = sample_timestamps(timestamps_mode, ep_num_images, fps) - num_frames = len(timestamps) - result = { - "psnr_values": [], - "ssim_values": [], - "mse_values": [], - } - - with time_benchmark, lock: - frames = decode_video_frames(video_path, timestamps=timestamps, tolerance_s=5e-1, backend=backend) - result["load_time_video_ms"] = (time_benchmark.last * 1000) / num_frames - - with time_benchmark: - original_frames = load_original_frames(imgs_dir, timestamps, fps) - result["load_time_images_ms"] = (time_benchmark.last * 1000) / num_frames - - frames_np, original_frames_np = frames.numpy(), original_frames.numpy() - for i in range(num_frames): - result["mse_values"].append(mean_squared_error(original_frames_np[i], frames_np[i])) - result["psnr_values"].append( - peak_signal_noise_ratio(original_frames_np[i], frames_np[i], data_range=1.0) - ) - result["ssim_values"].append( - structural_similarity(original_frames_np[i], frames_np[i], data_range=1.0, channel_axis=0) - ) - - if save_frames and sample == 0: - save_dir = video_path.with_suffix("") / f"{timestamps_mode}_{backend}" - save_decoded_frames(imgs_dir, save_dir, frames, timestamps, fps) - - return result - - load_times_video_ms = [] - load_times_images_ms = [] - mse_values = [] - psnr_values = [] - ssim_values = [] - - # A sample is a single set of decoded frames specified by timestamps_mode (e.g. a single frame, 2 frames, etc.). - # For each sample, we record metrics (loading time and quality metrics) which are then averaged over all samples. - # As these samples are independent, we run them in parallel threads to speed up the benchmark. - # Use a single shared lock for all worker threads - shared_lock = Lock() - with ThreadPoolExecutor(max_workers=num_workers) as executor: - futures = [executor.submit(process_sample, i, shared_lock) for i in range(num_samples)] - for future in tqdm(as_completed(futures), total=num_samples, desc="samples", leave=False): - result = future.result() - load_times_video_ms.append(result["load_time_video_ms"]) - load_times_images_ms.append(result["load_time_images_ms"]) - psnr_values.extend(result["psnr_values"]) - ssim_values.extend(result["ssim_values"]) - mse_values.extend(result["mse_values"]) - - avg_load_time_video_ms = float(np.array(load_times_video_ms).mean()) - avg_load_time_images_ms = float(np.array(load_times_images_ms).mean()) - video_images_load_time_ratio = avg_load_time_video_ms / avg_load_time_images_ms - - return { - "avg_load_time_video_ms": avg_load_time_video_ms, - "avg_load_time_images_ms": avg_load_time_images_ms, - "video_images_load_time_ratio": video_images_load_time_ratio, - "avg_mse": float(np.mean(mse_values)), - "avg_psnr": float(np.mean(psnr_values)), - "avg_ssim": float(np.mean(ssim_values)), - } - - -def benchmark_encoding_decoding( - dataset: LeRobotDataset, - video_path: Path, - imgs_dir: Path, - encoding_cfg: dict, - decoding_cfg: dict, - num_samples: int, - num_workers: int, - save_frames: bool, - overwrite: bool = False, - seed: int = 1337, -) -> list[dict]: - fps = dataset.fps - - if overwrite or not video_path.is_file(): - tqdm.write(f"encoding {video_path}") - encode_video_frames( - imgs_dir=imgs_dir, - video_path=video_path, - fps=fps, - vcodec=encoding_cfg["vcodec"], - pix_fmt=encoding_cfg["pix_fmt"], - g=encoding_cfg.get("g"), - crf=encoding_cfg.get("crf"), - # fast_decode=encoding_cfg.get("fastdecode"), - overwrite=True, - ) - - episode_index = 0 - ep_num_images = dataset.meta.episodes["length"][episode_index] - width, height = tuple(dataset[0][dataset.meta.camera_keys[0]].shape[-2:]) - num_pixels = width * height - video_size_bytes = video_path.stat().st_size - images_size_bytes = get_directory_size(imgs_dir) - video_images_size_ratio = video_size_bytes / images_size_bytes - - random.seed(seed) - benchmark_table = [] - for timestamps_mode in tqdm( - decoding_cfg["timestamps_modes"], desc="decodings (timestamps_modes)", leave=False - ): - for backend in tqdm(decoding_cfg["backends"], desc="decodings (backends)", leave=False): - benchmark_row = benchmark_decoding( - imgs_dir, - video_path, - timestamps_mode, - backend, - ep_num_images, - fps, - num_samples, - num_workers, - save_frames, - ) - benchmark_row.update( - **{ - "repo_id": dataset.repo_id, - "resolution": f"{width} x {height}", - "num_pixels": num_pixels, - "video_size_bytes": video_size_bytes, - "images_size_bytes": images_size_bytes, - "video_images_size_ratio": video_images_size_ratio, - "timestamps_mode": timestamps_mode, - "backend": backend, - }, - **encoding_cfg, - ) - benchmark_table.append(benchmark_row) - - return benchmark_table - - -def main( - output_dir: Path, - repo_ids: list[str], - vcodec: list[str], - pix_fmt: list[str], - g: list[int], - crf: list[int], - # fastdecode: list[int], - timestamps_modes: list[str], - backends: list[str], - num_samples: int, - num_workers: int, - save_frames: bool, -): - check_datasets_formats(repo_ids) - encoding_benchmarks = { - "g": g, - "crf": crf, - # "fastdecode": fastdecode, - } - decoding_benchmarks = { - "timestamps_modes": timestamps_modes, - "backends": backends, - } - headers = ["repo_id", "resolution", "num_pixels"] - headers += list(BASE_ENCODING.keys()) - headers += [ - "timestamps_mode", - "backend", - "video_size_bytes", - "images_size_bytes", - "video_images_size_ratio", - "avg_load_time_video_ms", - "avg_load_time_images_ms", - "video_images_load_time_ratio", - "avg_mse", - "avg_psnr", - "avg_ssim", - ] - file_paths = [] - for video_codec in tqdm(vcodec, desc="encodings (vcodec)"): - for pixel_format in tqdm(pix_fmt, desc="encodings (pix_fmt)", leave=False): - benchmark_table = [] - for repo_id in tqdm(repo_ids, desc="encodings (datasets)", leave=False): - dataset = LeRobotDataset(repo_id) - imgs_dir = output_dir / "images" / dataset.repo_id.replace("/", "_") - # We only use the first episode - save_first_episode(imgs_dir, dataset) - for duet in [ - dict(zip(encoding_benchmarks.keys(), unique_combination, strict=False)) - for unique_combination in itertools.product(*encoding_benchmarks.values()) - ]: - encoding_cfg = BASE_ENCODING.copy() - encoding_cfg["vcodec"] = video_codec - encoding_cfg["pix_fmt"] = pixel_format - for key, value in duet.items(): - encoding_cfg[key] = value - args_path = Path("_".join(str(value) for value in encoding_cfg.values())) - video_path = output_dir / "videos" / args_path / f"{repo_id.replace('/', '_')}.mp4" - benchmark_table += benchmark_encoding_decoding( - dataset, - video_path, - imgs_dir, - encoding_cfg, - decoding_benchmarks, - num_samples, - num_workers, - save_frames, - ) - - # Save intermediate results - benchmark_df = pd.DataFrame(benchmark_table, columns=headers) - now = dt.datetime.now() - csv_path = ( - output_dir - / f"{now:%Y-%m-%d}_{now:%H-%M-%S}_{video_codec}_{pixel_format}_{num_samples}-samples.csv" - ) - benchmark_df.to_csv(csv_path, header=True, index=False) - file_paths.append(csv_path) - del benchmark_df - - # Concatenate all results - df_list = [pd.read_csv(csv_path) for csv_path in file_paths] - concatenated_df = pd.concat(df_list, ignore_index=True) - concatenated_path = output_dir / f"{now:%Y-%m-%d}_{now:%H-%M-%S}_all_{num_samples}-samples.csv" - concatenated_df.to_csv(concatenated_path, header=True, index=False) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--output-dir", - type=Path, - default=Path("outputs/video_benchmark"), - help="Directory where the video benchmark outputs are written.", - ) - parser.add_argument( - "--repo-ids", - type=str, - nargs="*", - default=[ - "lerobot/pusht_image", - "lerobot/aloha_mobile_shrimp_image", - "lerobot/paris_street", - "lerobot/kitchen", - ], - help="Datasets repo-ids to test against. First episodes only are used. Must be images.", - ) - parser.add_argument( - "--vcodec", - type=str, - nargs="*", - default=["h264", "hevc", "libsvtav1"], - help="Video codecs to be tested", - ) - parser.add_argument( - "--pix-fmt", - type=str, - nargs="*", - default=["yuv444p", "yuv420p"], - help="Pixel formats (chroma subsampling) to be tested", - ) - parser.add_argument( - "--g", - type=parse_int_or_none, - nargs="*", - default=[1, 2, 3, 4, 5, 6, 10, 15, 20, 40, 100, None], - help="Group of pictures sizes to be tested.", - ) - parser.add_argument( - "--crf", - type=parse_int_or_none, - nargs="*", - default=[0, 5, 10, 15, 20, 25, 30, 40, 50, None], - help="Constant rate factors to be tested.", - ) - # parser.add_argument( - # "--fastdecode", - # type=int, - # nargs="*", - # default=[0, 1], - # help="Use the fastdecode tuning option. 0 disables it. " - # "For libx264 and libx265/hevc, only 1 is possible. " - # "For libsvtav1, 1, 2 or 3 are possible values with a higher number meaning a faster decoding optimization", - # ) - parser.add_argument( - "--timestamps-modes", - type=str, - nargs="*", - default=[ - "1_frame", - "2_frames", - "2_frames_4_space", - "6_frames", - ], - help="Timestamps scenarios to be tested.", - ) - parser.add_argument( - "--backends", - type=str, - nargs="*", - default=["torchcodec", "pyav"], - help="Torchvision decoding backend to be tested.", - ) - parser.add_argument( - "--num-samples", - type=int, - default=50, - help="Number of samples for each encoding x decoding config.", - ) - parser.add_argument( - "--num-workers", - type=int, - default=10, - help="Number of processes for parallelized sample processing.", - ) - parser.add_argument( - "--save-frames", - type=int, - default=0, - help="Whether to save decoded frames or not. Enter a non-zero number for true.", - ) - args = parser.parse_args() - main(**vars(args)) diff --git a/pyproject.toml b/pyproject.toml index 870f7b62b..f983134ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,18 @@ dataset = [ "pandas>=2.0.0,<3.0.0", # NOTE: Transitive dependency of datasets "pyarrow>=21.0.0,<30.0.0", # NOTE: Transitive dependency of datasets "lerobot[av-dep]", - "torchcodec>=0.3.0,<0.12.0; sys_platform != 'win32' and (sys_platform != 'linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l')) and (sys_platform != 'darwin' or platform_machine != 'x86_64')", # NOTE: Windows support starts at version 0.7 (needs torch==2.8), ffmpeg>=8 support starts at version 0.8.1 (needs torch==2.9), system-wide ffmpeg support starts at version 0.10 (needs torch==2.10), 0.11 needs torch==2.11, 0.12 needs torch==2.12. + + # NOTE: torchcodec wheel availability matrix (PyPI): + # - linux x86_64/amd64 + macOS arm64 : wheels since 0.3.0 (the historic supported set). + # - win32 x86_64 : wheels since 0.7.0 (needs torch>=2.8). + # - linux aarch64/arm64 : wheels since 0.11.0 (needs torch>=2.11). + # - macOS x86_64 (Intel) and linux armv7l: no wheels in any released version -> fall through to the PyAV decoder. + # Each platform gets its own line so the resolver picks the minimum version that has a wheel for it. + + # Other torch/torchcodec pairings (informational): 0.8.1 = ffmpeg>=8 support, 0.10 = system-wide ffmpeg support, 0.12 needs torch==2.12. + "torchcodec>=0.3.0,<0.12.0; (sys_platform == 'linux' and (platform_machine == 'x86_64' or platform_machine == 'AMD64')) or (sys_platform == 'darwin' and platform_machine == 'arm64')", + "torchcodec>=0.7.0,<0.12.0; sys_platform == 'win32'", + "torchcodec>=0.11.0,<0.12.0; sys_platform == 'linux' and (platform_machine == 'aarch64' or platform_machine == 'arm64')", "jsonlines>=4.0.0,<5.0.0", ] training = [ diff --git a/src/lerobot/datasets/video_utils.py b/src/lerobot/datasets/video_utils.py index 512dc6d9b..00ff09ee7 100644 --- a/src/lerobot/datasets/video_utils.py +++ b/src/lerobot/datasets/video_utils.py @@ -33,7 +33,6 @@ import fsspec import numpy as np import pyarrow as pa import torch -import torchvision from datasets.features.features import register_feature from PIL import Image @@ -132,7 +131,9 @@ def decode_video_frames( video_path (Path): Path to the video file. timestamps (list[float]): List of timestamps to extract frames. tolerance_s (float): Allowed deviation in seconds for frame retrieval. - backend (str, optional): Backend to use for decoding. Defaults to "torchcodec" when available in the platform; otherwise, defaults to "pyav". + backend (str, optional): Backend to use for decoding. Defaults to "torchcodec" when available + in the platform; otherwise, defaults to "pyav". The legacy value "video_reader" is + accepted for one release as an alias for "pyav" and will be removed in a future version. return_uint8 (bool): If True, return raw uint8 frames without float32 normalization. This reduces memory for DataLoader IPC; normalization can be done on GPU afterward. @@ -145,85 +146,87 @@ def decode_video_frames( backend = get_safe_default_codec() if backend == "torchcodec": return decode_video_frames_torchcodec(video_path, timestamps, tolerance_s, return_uint8=return_uint8) - elif backend in ["pyav", "video_reader"]: - return decode_video_frames_torchvision( - video_path, timestamps, tolerance_s, backend, return_uint8=return_uint8 - ) + elif backend == "pyav": + return decode_video_frames_pyav(video_path, timestamps, tolerance_s, return_uint8=return_uint8) + elif backend == "video_reader": + logger.warning("backend='video_reader' is deprecated and now aliases to 'pyav'.") + return decode_video_frames_pyav(video_path, timestamps, tolerance_s, return_uint8=return_uint8) else: raise ValueError(f"Unsupported video backend: {backend}") -def decode_video_frames_torchvision( +def decode_video_frames_pyav( video_path: Path | str, timestamps: list[float], tolerance_s: float, - backend: str = "pyav", log_loaded_timestamps: bool = False, return_uint8: bool = False, ) -> torch.Tensor: - """Loads frames associated to the requested timestamps of a video + """Loads frames associated to the requested timestamps of a video using PyAV. - The backend can be either "pyav" (default) or "video_reader". - "video_reader" requires installing torchvision from source, see: - https://github.com/pytorch/vision/blob/main/torchvision/csrc/io/decoder/gpu/README.rst - (note that you need to compile against ffmpeg<4.3) + This is the fallback decoder for platforms where torchcodec has no wheel (currently macOS + x86_64 and linux armv7l — see the torchcodec block in pyproject.toml for the full matrix). + On supported platforms, prefer `decode_video_frames_torchcodec`, which is faster and supports + accurate seek. - While both use cpu, "video_reader" is supposedly faster than "pyav" but requires additional setup. - For more info on video decoding, see `benchmark/video/README.md` + PyAV doesn't support accurate seek: we seek to the nearest preceding keyframe and decode + forward until we have covered the requested timestamp range. The number of key frames in a + video can be adjusted at encoding time to trade off decoding speed against file size. - See torchvision doc for more info on these two backends: - https://pytorch.org/vision/0.18/index.html?highlight=backend#torchvision.set_video_backend + Args: + video_path: Path to the video file. + timestamps: List of timestamps (in seconds) to extract frames for. + tolerance_s: Allowed deviation in seconds between a queried timestamp and the closest + decoded frame. + log_loaded_timestamps: When True, log every decoded frame's timestamp at INFO level. + return_uint8: When True, return raw uint8 frames (C, H, W). Otherwise, return float32 in + [0, 1] range. - Note: Video benefits from inter-frame compression. Instead of storing every frame individually, - the encoder stores a reference frame (or a key frame) and subsequent frames as differences relative to - that key frame. As a consequence, to access a requested frame, we need to load the preceding key frame, - and all subsequent frames until reaching the requested frame. The number of key frames in a video - can be adjusted during encoding to take into account decoding time and video size in bytes. + Returns: + torch.Tensor of shape (len(timestamps), C, H, W). """ - video_path = str(video_path) - - # set backend - keyframes_only = False - torchvision.set_video_backend(backend) - if backend == "pyav": - keyframes_only = True # pyav doesn't support accurate seek - - # set a video stream reader # TODO(rcadene): also load audio stream at the same time - reader = torchvision.io.VideoReader(video_path, "video") + video_path = str(video_path) # set the first and last requested timestamps # Note: previous timestamps are usually loaded, since we need to access the previous key frame first_ts = min(timestamps) last_ts = max(timestamps) - # access closest key frame of the first requested frame - # Note: closest key frame timestamp is usually smaller than `first_ts` (e.g. key frame can be the first frame of the video) - # for details on what `seek` is doing see: https://pyav.basswood-io.com/docs/stable/api/container.html?highlight=inputcontainer#av.container.InputContainer.seek - reader.seek(first_ts, keyframes_only=keyframes_only) + loaded_frames: list[torch.Tensor] = [] + loaded_ts: list[float] = [] - # load all frames until last requested frame - loaded_frames = [] - loaded_ts = [] - for frame in reader: - current_ts = frame["pts"] - if log_loaded_timestamps: - logger.info(f"frame loaded at timestamp={current_ts:.4f}") - loaded_frames.append(frame["data"]) - loaded_ts.append(current_ts) - if current_ts >= last_ts: - break + # Seek + decode. `container.seek(offset)` with no `stream` argument expects the offset in + # av.time_base units (microseconds). `backward=True` lands us on the nearest keyframe at or + # before `first_ts`, so we can then decode forward until we cover `last_ts`. See: + # https://pyav.basswood-io.com/docs/stable/api/container.html#av.container.InputContainer.seek + with av.open(video_path) as container: + stream = container.streams.video[0] + container.seek(int(first_ts * av.time_base), backward=True) - if backend == "pyav": - reader.container.close() + for frame in container.decode(stream): + if frame.pts is None: + continue + current_ts = float(frame.pts * stream.time_base) + if log_loaded_timestamps: + logger.info(f"frame loaded at timestamp={current_ts:.4f}") + # Convert to CHW uint8 to match torchcodec's output layout. + arr = frame.to_ndarray(format="rgb24") # H, W, 3 + loaded_frames.append(torch.from_numpy(arr).permute(2, 0, 1).contiguous()) + loaded_ts.append(current_ts) + if current_ts >= last_ts: + break - reader = None + if not loaded_frames: + raise FrameTimestampError( + f"No frames could be decoded from {video_path} in the timestamp range [{first_ts}, {last_ts}]." + ) query_ts = torch.tensor(timestamps) - loaded_ts = torch.tensor(loaded_ts) + loaded_ts_t = torch.tensor(loaded_ts) # compute distances between each query timestamp and timestamps of all loaded frames - dist = torch.cdist(query_ts[:, None], loaded_ts[:, None], p=1) + dist = torch.cdist(query_ts[:, None], loaded_ts_t[:, None], p=1) min_, argmin_ = dist.min(1) is_within_tol = min_ < tolerance_s @@ -234,14 +237,14 @@ def decode_video_frames_torchvision( " This might be due to synchronization issues with timestamps during data collection." " To be safe, we advise to ignore this item during training." f"\nqueried timestamps: {query_ts}" - f"\nloaded timestamps: {loaded_ts}" + f"\nloaded timestamps: {loaded_ts_t}" f"\nvideo: {video_path}" - f"\nbackend: {backend}" + f"\nbackend: pyav" ) # get closest frames to the query timestamps closest_frames = torch.stack([loaded_frames[idx] for idx in argmin_]) - closest_ts = loaded_ts[argmin_] + closest_ts = loaded_ts_t[argmin_] if log_loaded_timestamps: logger.info(f"{closest_ts=}") diff --git a/src/lerobot/scripts/lerobot_info.py b/src/lerobot/scripts/lerobot_info.py index 2092db48b..a057836e5 100644 --- a/src/lerobot/scripts/lerobot_info.py +++ b/src/lerobot/scripts/lerobot_info.py @@ -92,6 +92,7 @@ def get_sys_info() -> dict[str, str]: info.update( { "PyTorch version": torch_version, + "Torchcodec version": get_package_version("torchcodec"), "Is PyTorch built with CUDA support?": str(torch_cuda_available), "Cuda version": cuda_version, "GPU model": gpu_model, diff --git a/uv.lock b/uv.lock index 28b906d89..2ffaf9fe4 100644 --- a/uv.lock +++ b/uv.lock @@ -2,13 +2,17 @@ version = 1 revision = 3 requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '3.15' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version >= '3.15' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version >= '3.15' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'linux'", - "python_full_version == '3.14.*' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version == '3.14.*' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.14.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "(python_full_version == '3.13.*' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'linux'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'linux'", - "python_full_version < '3.13' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version < '3.13' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version < '3.13' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'linux'", "(python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'armv7l' and sys_platform == 'linux')", "(python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", @@ -1134,7 +1138,7 @@ name = "decord" version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x') or (platform_machine != 's390x' and sys_platform != 'linux')" }, + { name = "numpy", marker = "(platform_machine != 's390x' and sys_platform != 'linux') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] 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" }, @@ -2729,7 +2733,7 @@ all = [ { name = "scikit-image" }, { name = "scipy" }, { name = "teleop" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "torchcodec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'win32'" }, { name = "torchdiffeq" }, { name = "transformers" }, { name = "wandb" }, @@ -2742,7 +2746,7 @@ aloha = [ { name = "pandas" }, { name = "pyarrow" }, { name = "scipy" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "torchcodec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'win32'" }, ] async = [ { name = "contourpy" }, @@ -2766,7 +2770,7 @@ core-scripts = [ { name = "pynput" }, { name = "pyserial" }, { name = "rerun-sdk" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "torchcodec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'win32'" }, ] damiao = [ { name = "python-can" }, @@ -2777,7 +2781,7 @@ dataset = [ { name = "jsonlines" }, { name = "pandas" }, { name = "pyarrow" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "torchcodec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'win32'" }, ] dataset-viz = [ { name = "av" }, @@ -2786,7 +2790,7 @@ dataset-viz = [ { name = "pandas" }, { name = "pyarrow" }, { name = "rerun-sdk" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "torchcodec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'win32'" }, ] deepdiff-dep = [ { name = "deepdiff" }, @@ -2888,7 +2892,7 @@ libero = [ { name = "pandas" }, { name = "pyarrow" }, { name = "scipy" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "torchcodec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'win32'" }, { name = "transformers" }, ] matplotlib-dep = [ @@ -2903,7 +2907,7 @@ metaworld = [ { name = "pandas" }, { name = "pyarrow" }, { name = "scipy" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "torchcodec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'win32'" }, ] multi-task-dit = [ { name = "diffusers" }, @@ -2944,7 +2948,7 @@ pusht = [ { name = "pandas" }, { name = "pyarrow" }, { name = "pymunk" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "torchcodec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'win32'" }, ] pygame-dep = [ { name = "pygame" }, @@ -2996,7 +3000,7 @@ training = [ { name = "jsonlines" }, { name = "pandas" }, { name = "pyarrow" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "torchcodec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'win32'" }, { name = "wandb" }, ] transformers-dep = [ @@ -3209,7 +3213,9 @@ requires-dist = [ { name = "timm", marker = "extra == 'groot'", specifier = ">=1.0.0,<1.1.0" }, { name = "torch", marker = "sys_platform != 'linux'", specifier = ">=2.7,<2.12.0" }, { name = "torch", marker = "sys_platform == 'linux'", specifier = ">=2.7,<2.12.0", index = "https://download.pytorch.org/whl/cu128" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux' and extra == 'dataset') or (platform_machine != 'x86_64' and sys_platform == 'darwin' and extra == 'dataset') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'dataset')", specifier = ">=0.3.0,<0.12.0" }, + { name = "torchcodec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin' and extra == 'dataset') or (platform_machine == 'AMD64' and sys_platform == 'linux' and extra == 'dataset') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'dataset')", specifier = ">=0.3.0,<0.12.0" }, + { name = "torchcodec", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'dataset') or (platform_machine == 'arm64' and sys_platform == 'linux' and extra == 'dataset')", specifier = ">=0.11.0,<0.12.0" }, + { name = "torchcodec", marker = "sys_platform == 'win32' and extra == 'dataset'", specifier = ">=0.7.0,<0.12.0" }, { name = "torchdiffeq", marker = "extra == 'wallx'", specifier = ">=0.2.4,<0.3.0" }, { name = "torchvision", marker = "sys_platform != 'linux'", specifier = ">=0.22.0,<0.27.0" }, { name = "torchvision", marker = "sys_platform == 'linux'", specifier = ">=0.22.0,<0.27.0", index = "https://download.pytorch.org/whl/cu128" }, @@ -4260,6 +4266,7 @@ dependencies = [ { name = "protobuf" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/81/b1/d111b1df656761f980d9e298a60039a9cb66036b1d039e777537743d0ac3/onnxruntime-1.26.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05b028781b322ad74b57ce5b50aa5280bb1fe96ceec334628ade681e0b24c1ac", size = 18016624, upload-time = "2026-05-12T00:41:01.735Z" }, { url = "https://files.pythonhosted.org/packages/f6/a0/3f9d896a0385a36bd04345d6d0b802821a5782adde562e7e135f6bb71c73/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91f2bb870a4b9224eba0a6728c1fa7a9e552b8e59e1083c51fbbc3d013f2b5c0", size = 16052692, upload-time = "2026-05-08T19:07:13.829Z" }, { url = "https://files.pythonhosted.org/packages/7c/43/2a4e04f8dbeffad19bbcced4bcd4289bf478921518437404d6b92bdf213b/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b6dd70599005bd1bf29779f04a91978b92b5e719c11a20068a8f8e535f725b6", size = 18185439, upload-time = "2026-05-08T19:07:36.299Z" }, { url = "https://files.pythonhosted.org/packages/44/fc/026d0a7162b9c2153dac292baea9e027c42304dc1d9dc6f8ff5b4cfbaedd/onnxruntime-1.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:a26374dc7fbcaae593601086b242120e13f2310558df0991da6dd8b8fac00414", size = 13026427, upload-time = "2026-05-08T19:08:03.503Z" }, @@ -6275,13 +6282,17 @@ name = "torch" version = "2.11.0+cu128" source = { registry = "https://download.pytorch.org/whl/cu128" } resolution-markers = [ - "python_full_version >= '3.15' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version >= '3.15' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version >= '3.15' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'linux'", - "python_full_version == '3.14.*' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version == '3.14.*' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.14.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "(python_full_version == '3.13.*' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'linux'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'linux'", - "python_full_version < '3.13' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version < '3.13' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version < '3.13' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'linux'", "(python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'armv7l' and sys_platform == 'linux')", "(python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", @@ -6325,12 +6336,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/85/38f4843ff2a6bf7dfb71a153acd99024dadb96749965a67524c2f1cc1894/torchcodec-0.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:57056e91d1d883d0fb77ca7759e304be9c0bdb4ea0e37bde5c2e361347063b8c", size = 4368988, upload-time = "2026-04-14T18:24:51.46Z" }, { url = "https://files.pythonhosted.org/packages/4b/85/3b41034b0f1289423745f918ace2a1e1e86b9c578c2e2461b6afcbb5354a/torchcodec-0.11.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f1aee486a84247fcaa67870ac5005aa8d382a9839e91e476fa71b5b3d9fda9b7", size = 2397532, upload-time = "2026-04-14T18:24:53.368Z" }, { url = "https://files.pythonhosted.org/packages/ca/a9/a2b6ee3e84c55bdd0c45fd991dde71c95a99115ec9e26938b212b4545dcf/torchcodec-0.11.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6c26e90e7aa982302644d0af8cb706318682bb390f48a80ecbfeab03499acd04", size = 2329883, upload-time = "2026-04-14T18:24:55.467Z" }, + { url = "https://files.pythonhosted.org/packages/82/48/683114a4ed6b59f76b6919532a5db0f4068787be26bab92cc18a1dfa6794/torchcodec-0.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:3fd2d10e0e0a5f455c1c87dc1380b3bd43b77dd5eeeaf479470643b1c04a2dd2", size = 1921066, upload-time = "2026-04-14T18:24:57.102Z" }, { url = "https://files.pythonhosted.org/packages/2c/61/a8985a7561ef651e409deeac151a0ed5cef763db9577db5cc49c2f5eaab2/torchcodec-0.11.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:915fbe20068ec77486fbbeaf0c627c89c7376445f27d215b7489c0a03c64fd4c", size = 4289805, upload-time = "2026-04-14T18:24:59.124Z" }, { url = "https://files.pythonhosted.org/packages/7a/31/c4ec0304dd169a9b2b7fa0dd1d5d659d3cccc975b98ac88c498fe6dd7196/torchcodec-0.11.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3755de03c96afd37410cba68198225d11cd6431a32f2161a0019791a4a853305", size = 2399057, upload-time = "2026-04-14T18:25:00.782Z" }, { url = "https://files.pythonhosted.org/packages/5d/b2/85ad7a81f387e40983c21bc94da0c333974afb41f38c3a85d25875274187/torchcodec-0.11.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5eee69971cec1147a03b8a6b678b5dfbeff0b2c71ed7929e488391f9fbcd630c", size = 2332721, upload-time = "2026-04-14T18:25:02.518Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ca/5c66f21d2a12039450e9dd4d9d7c480019dfbe9e8a87696a3c3a827c1e37/torchcodec-0.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:67b34e5733636588ebe0f15082bbb90a8ce1472ccb8bb1a656ec28958a208919", size = 1920990, upload-time = "2026-04-14T18:25:04.269Z" }, { url = "https://files.pythonhosted.org/packages/c4/b7/8d6ee76fca0cfefec01402f33c11766455da2b8460cb9191cdc34f8defc0/torchcodec-0.11.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:a00ef79e847644f91c9995de021062adc851916b16244d26c0a7a04569710508", size = 4408290, upload-time = "2026-04-14T18:25:05.967Z" }, { url = "https://files.pythonhosted.org/packages/1e/1e/e37bd46ffac9eec1a9afc32c5097cd83b0de1e865021f7f953c5142919f4/torchcodec-0.11.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:170a3efea64f0cd2c21cee0a233a9e13c67a704b5c5e7ef9aeda31e747ac6885", size = 2402232, upload-time = "2026-04-14T18:25:08.026Z" }, { url = "https://files.pythonhosted.org/packages/8f/d0/a9173dbfa011cc2224f7489e50844b9f62110050bbdbd9d29485e7f1e0e2/torchcodec-0.11.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:db66ddce36a6fa35f30fbe1d78b57289fcb53f8f43c1c85923edbe339540c665", size = 2334158, upload-time = "2026-04-14T18:25:09.77Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/6ee0e26547976dc55a69042ce895747a34221eab348931e975141d80d25e/torchcodec-0.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:3fd9ef8302b261d3db5585e42be4a3138c5c240a822031642cdf1f82ea3db5b7", size = 1925002, upload-time = "2026-04-14T18:25:11.718Z" }, ] [[package]] @@ -6400,13 +6414,17 @@ name = "torchvision" version = "0.26.0+cu128" source = { registry = "https://download.pytorch.org/whl/cu128" } resolution-markers = [ - "python_full_version >= '3.15' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version >= '3.15' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version >= '3.15' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'linux'", - "python_full_version == '3.14.*' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version == '3.14.*' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.14.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "(python_full_version == '3.13.*' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version == '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'linux'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'linux'", - "python_full_version < '3.13' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version < '3.13' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "python_full_version < '3.13' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'linux'", "(python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'armv7l' and sys_platform == 'linux')", "(python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", From f218d5ab30241bf5060907699c91583d0a302b61 Mon Sep 17 00:00:00 2001 From: Caroline Pascal Date: Tue, 12 May 2026 20:44:11 +0200 Subject: [PATCH 11/18] feat(episodes): adding support for metadata based episodes filtering (#3530) * feat(episode filtering): adding support for episodes filtering at initialization time in LeRobotDataset * test(tests): adding tests * chore(format): formatting code * feat(performance): improving implementation for better performances on big datasets * chores(warning): improving warnings and errors for episodes filtering * test(invalid key): adding test for invalid filtering key * chore(format): formatting code --- src/lerobot/datasets/dataset_metadata.py | 24 +++++++++ src/lerobot/datasets/lerobot_dataset.py | 24 ++++++++- tests/datasets/test_datasets.py | 65 ++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/lerobot/datasets/dataset_metadata.py b/src/lerobot/datasets/dataset_metadata.py index 4f89ba2a4..b404ddb18 100644 --- a/src/lerobot/datasets/dataset_metadata.py +++ b/src/lerobot/datasets/dataset_metadata.py @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import contextlib +from collections.abc import Callable from pathlib import Path import numpy as np @@ -189,6 +190,29 @@ class LeRobotDatasetMetadata: if self.episodes is None: self._load_metadata() + def filter_episodes( + self, + predicate: Callable[[dict], bool], + candidates: list[int] | None = None, + ) -> list[int]: + """Filter episodes whose metadata satisfies a given predicate. + + Args: + predicate: Predicate over per-episode metadata rows used to select episodes. + candidates: Optional list of episode indices to restrict evaluation to. + + Returns: + List of sorted episode indices that satisfy the predicate. + """ + self.ensure_readable() + if candidates is not None: + candidate_set = set(candidates) + combined = lambda ep: ep["episode_index"] in candidate_set and predicate(ep) # noqa: E731 + else: + combined = predicate + filtered = self.episodes.filter(combined, keep_in_memory=True, load_from_cache_file=False) + return sorted(int(idx) for idx in filtered["episode_index"]) + def _pull_from_repo( self, allow_patterns: list[str] | str | None = None, diff --git a/src/lerobot/datasets/lerobot_dataset.py b/src/lerobot/datasets/lerobot_dataset.py index b6ab0f5f0..ab55aa9f8 100644 --- a/src/lerobot/datasets/lerobot_dataset.py +++ b/src/lerobot/datasets/lerobot_dataset.py @@ -49,6 +49,7 @@ class LeRobotDataset(torch.utils.data.Dataset): repo_id: str, root: str | Path | None = None, episodes: list[int] | None = None, + episode_filter: Callable[[dict], bool] | None = None, image_transforms: Callable | None = None, delta_timestamps: dict[str, list[float]] | None = None, tolerance_s: float = 1e-4, @@ -153,6 +154,11 @@ class LeRobotDataset(torch.utils.data.Dataset): ``$HF_LEROBOT_HOME/hub``. episodes (list[int] | None, optional): If specified, this will only load episodes specified by their episode_index in this list. Defaults to None. + episode_filter (Callable[[dict], bool] | None, optional): Predicate over per-episode + metadata rows used to select episodes. Evaluated against ``meta/`` without ``stats`` keys + (e.g.``task_index``, ``episode_index``, ``length``, ``from_timestamp``, ``to_timestamp``). + Intersected with ``episodes`` when both are set. Example: ``lambda ep: ep["length"] >= 100``. + Defaults to None. image_transforms (Callable | None, optional): Transform applied to visual modalities inside `__getitem__` after image decoding / tensor conversion. This works for both image-backed and video-backed observations and can later be @@ -199,7 +205,6 @@ class LeRobotDataset(torch.utils.data.Dataset): self.reader = None self.set_image_transforms(image_transforms) self.delta_timestamps = delta_timestamps - self.episodes = episodes self.tolerance_s = tolerance_s self.revision = revision if revision else CODEBASE_VERSION self._video_backend = video_backend if video_backend else get_safe_default_codec() @@ -218,6 +223,23 @@ class LeRobotDataset(torch.utils.data.Dataset): self.root = self.meta.root self.revision = self.meta.revision + if episodes is not None and any( + episode >= self.meta.total_episodes or episode < 0 for episode in episodes + ): + logger.warning( + f"Some episodes in the provided episodes list are out of range for this dataset ({self.meta.total_episodes})." + ) + + if episode_filter is not None: + resolved = self.meta.filter_episodes(episode_filter, candidates=episodes) + if not resolved: + raise ValueError( + "The episode filter did not match any episode. Make sure the filter and episodes list are valid and compatible." + ) + logger.info(f"The episode filter matched {len(resolved)} episode(s).") + episodes = resolved + self.episodes = episodes + # Create reader (hf_dataset loaded below) self.reader = DatasetReader( meta=self.meta, diff --git a/tests/datasets/test_datasets.py b/tests/datasets/test_datasets.py index 3e1a17a62..654f8cdf1 100644 --- a/tests/datasets/test_datasets.py +++ b/tests/datasets/test_datasets.py @@ -1691,3 +1691,68 @@ def test_delta_timestamps_query_returns_correct_values(tmp_path, empty_lerobot_d # Previous frame is outside episode, so it's clamped to first frame and marked as padded assert state_values == [10.0, 10.0], f"Expected [10.0, 10.0], got {state_values}" assert is_pad == [True, False], f"Expected [True, False], got {is_pad}" + + +def test_episode_filter_filters_dataset(tmp_path, lerobot_dataset_factory): + """episode_filter on LeRobotDataset narrows the loaded dataset to matching episodes.""" + dataset = lerobot_dataset_factory(root=tmp_path / "test", total_episodes=8, total_frames=200) + lengths = dataset.meta.episodes["length"] + threshold = sorted(lengths)[len(lengths) // 2] + expected_eps = [i for i, length in enumerate(lengths) if length >= threshold] + expected_frames = sum(lengths[i] for i in expected_eps) + + filtered = LeRobotDataset( + dataset.repo_id, + root=dataset.root, + episode_filter=lambda ep: ep["length"] >= threshold, + ) + + assert filtered.num_episodes == len(expected_eps) + assert filtered.num_frames == expected_frames + seen_eps = {filtered[i]["episode_index"].item() for i in range(len(filtered))} + assert seen_eps == set(expected_eps) + + +def test_episode_filter_intersects_with_episodes(tmp_path, lerobot_dataset_factory): + """When both episodes and episode_filter are given to LeRobotDataset, the result is their intersection.""" + dataset = lerobot_dataset_factory(root=tmp_path / "test", total_episodes=8, total_frames=200) + lengths = dataset.meta.episodes["length"] + candidates = [0, 2, 4, 6] + candidate_lengths = [lengths[i] for i in candidates] + threshold = sorted(candidate_lengths)[len(candidate_lengths) // 2] + expected_eps = [i for i in candidates if lengths[i] >= threshold] + + filtered = LeRobotDataset( + dataset.repo_id, + root=dataset.root, + episodes=candidates, + episode_filter=lambda ep: ep["length"] >= threshold, + ) + + assert filtered.num_episodes == len(expected_eps) + seen_eps = {filtered[i]["episode_index"].item() for i in range(len(filtered))} + assert seen_eps == set(expected_eps) + + +def test_episode_filter_no_match_raises(tmp_path, lerobot_dataset_factory): + """An empty match in LeRobotDataset's episode_filter raises a ValueError rather than silently returning an empty dataset.""" + dataset = lerobot_dataset_factory(root=tmp_path / "test", total_episodes=4, total_frames=100) + + with pytest.raises(ValueError, match=r"The episode filter did not match any episode"): + LeRobotDataset( + dataset.repo_id, + root=dataset.root, + episode_filter=lambda ep: ep["length"] < 0, + ) + + +def test_episode_filter_unknown_key_raises(tmp_path, lerobot_dataset_factory): + """A predicate referencing a column absent from meta.episodes surfaces a clear KeyError.""" + dataset = lerobot_dataset_factory(root=tmp_path / "test", total_episodes=4, total_frames=100) + + with pytest.raises(KeyError, match="not_a_real_field"): + LeRobotDataset( + dataset.repo_id, + root=dataset.root, + episode_filter=lambda ep: ep["not_a_real_field"] > 0, + ) From 2438df13072d81c0422f81fb5e876fc1480d188b Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Tue, 12 May 2026 21:20:26 +0200 Subject: [PATCH 12/18] chore(dependencies): update uv.lock (#3561) --- uv.lock | 554 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 283 insertions(+), 271 deletions(-) diff --git a/uv.lock b/uv.lock index 2ffaf9fe4..408a9a351 100644 --- a/uv.lock +++ b/uv.lock @@ -1,32 +1,36 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12" resolution-markers = [ "(python_full_version >= '3.15' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'linux')", - "python_full_version >= '3.15' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", - "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'linux'", "(python_full_version == '3.14.*' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux')", - "python_full_version == '3.14.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "(python_full_version == '3.13.*' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')", - "python_full_version == '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "(python_full_version < '3.13' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'linux')", + "(python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'linux')", + "(python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'linux')", + "(python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'linux')", + "python_full_version >= '3.15' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'linux'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'linux'", - "(python_full_version < '3.13' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", - "python_full_version < '3.13' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'linux'", - "(python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'armv7l' and sys_platform == 'linux')", - "(python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", - "(python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", - "(python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'armv7l' and sys_platform == 'linux')", - "(python_full_version >= '3.15' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "(python_full_version >= '3.15' and platform_machine != 'arm64' and platform_machine != 's390x' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'emscripten'", "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'emscripten'", - "(python_full_version == '3.14.*' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version == '3.14.*' and platform_machine != 'arm64' and platform_machine != 's390x' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version == '3.13.*' and platform_machine != 'arm64' and platform_machine != 's390x' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", - "(python_full_version < '3.13' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version < '3.13' and platform_machine != 'arm64' and platform_machine != 's390x' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", @@ -34,13 +38,13 @@ resolution-markers = [ "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", - "(python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'win32'", "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'win32'", - "(python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'win32')", - "(python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'win32'", "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'win32'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'win32'", - "(python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", ] @@ -630,11 +634,11 @@ wheels = [ [[package]] name = "cmeel" -version = "0.60.0" +version = "0.60.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2b/a178a123602cb23b737289ae24fe9213bf1002660bb89d48e5dda62b46cc/cmeel-0.60.0.tar.gz", hash = "sha256:2e6d9ae61cc94112a67814b14948dd679b353090be4b87ab04c3ccaea3aa95de", size = 14935, upload-time = "2026-05-09T16:03:35.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/46/ddc7df697e49cae32b1a97b3e1a3b47b815238f9059312f987bc62a2e756/cmeel-0.60.1.tar.gz", hash = "sha256:3e0b92eb933a3693ad3f1da8aae31defcbee5f25969daaf20e59c57d6a9474cf", size = 14972, upload-time = "2026-05-11T17:13:57.216Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/d4/ffd1484c68ca7489596806f830446219540dd17508818fe0d2c2fb0f4f59/cmeel-0.60.0-py3-none-any.whl", hash = "sha256:ed0672f7cebbb1143e6e29fcc0d3fd26e100ed2381b49dd15444bd1dd6d3ce0b", size = 20573, upload-time = "2026-05-09T16:03:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/da/39/f2db2ff475d42222c70fe25c737028aeaafdd9c0aeba04e6b2dc66e7f93f/cmeel-0.60.1-py3-none-any.whl", hash = "sha256:3f92b68353a58b4d6b5a664ea96bf58a3fef0891a8ed570d3c153361bbbb94b7", size = 20612, upload-time = "2026-05-11T17:13:55.812Z" }, ] [[package]] @@ -916,86 +920,86 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.5" +version = "7.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, - { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, - { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, - { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, - { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, - { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, - { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, - { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, - { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, - { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, - { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, - { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, - { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, - { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, - { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, - { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, - { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, - { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, - { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, - { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, - { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, - { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, - { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, - { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, + { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, + { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, + { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, + { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, + { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, ] [[package]] @@ -1138,7 +1142,7 @@ name = "decord" version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "(platform_machine != 's390x' and sys_platform != 'linux') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "numpy", marker = "(platform_machine != 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] 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" }, @@ -1205,7 +1209,7 @@ wheels = [ [[package]] name = "dm-control" -version = "1.0.40" +version = "1.0.41" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "absl-py" }, @@ -1224,9 +1228,9 @@ dependencies = [ { name = "setuptools" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/27/1d3caa7fa7557b70d766f437979636b6a8c99b14f6e8b8f84795cad9f1df/dm_control-1.0.40.tar.gz", hash = "sha256:af5828af47fe50466008d53b141893a05c4e2779169fc8ef469d1828a016266e", size = 56273764, upload-time = "2026-04-25T22:05:39.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/49/10beb2d63b05e385bbb67995b6621385c4c80719ad1c71acfc55537b97de/dm_control-1.0.41.tar.gz", hash = "sha256:644113b7bbba5884da57e15adbb28961de775d04e2db8b1bf0304c06835eff16", size = 56275358, upload-time = "2026-05-11T16:27:17.895Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/eb/a762d6f15e6c4faccef8fdcdae50cf8f232800a36b70aef93d38a787bb58/dm_control-1.0.40-py3-none-any.whl", hash = "sha256:cd15b1d95f5b320b7924e518715b8ac132043d575588b7e122f21016f49c7e89", size = 56446428, upload-time = "2026-04-25T22:05:33.561Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/b4b082bdd27a4bfd689a4c4109b3fab828ac56db60b024f07710e496fa65/dm_control-1.0.41-py3-none-any.whl", hash = "sha256:81e89b295aeeca2ba71e8dd1baf26084f9d974f8b80a4a4c284da7b883847161", size = 56446431, upload-time = "2026-05-11T16:27:12.6Z" }, ] [[package]] @@ -1465,8 +1469,8 @@ name = "flash-attn" version = "2.8.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "einops" }, - { name = "torch", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "einops", marker = "platform_machine != 'arm64' or sys_platform != 'darwin'" }, + { name = "torch", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "torch", version = "2.11.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/b2/8d76c41ad7974ee264754709c22963447f7f8134613fd9ce80984ed0dab7/flash_attn-2.8.3.tar.gz", hash = "sha256:1e71dd64a9e0280e0447b8a0c2541bad4bf6ac65bdeaa2f90e51a9e57de0370d", size = 8447812, upload-time = "2025-08-15T08:28:12.911Z" } @@ -2065,11 +2069,11 @@ wheels = [ [[package]] name = "idna" -version = "3.13" +version = "3.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/b1/efac073e0c297ecf2fb33c346989a529d4e19164f1759102dee5953ee17e/idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3", size = 198272, upload-time = "2026-05-10T20:32:15.935Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3c/3f62dee257eb3d6b2c1ef2a09d36d9793c7111156a73b5654d2c2305e5ce/idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69", size = 72184, upload-time = "2026-05-10T20:32:14.295Z" }, ] [[package]] @@ -2427,7 +2431,7 @@ dependencies = [ { name = "nbformat" }, { name = "packaging" }, { name = "prometheus-client" }, - { name = "pywinpty", marker = "os_name == 'nt' and sys_platform != 'linux'" }, + { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "pyzmq" }, { name = "send2trash" }, { name = "terminado" }, @@ -2445,7 +2449,7 @@ name = "jupyter-server-terminals" version = "0.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywinpty", marker = "os_name == 'nt' and sys_platform != 'linux'" }, + { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') 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" } @@ -2515,7 +2519,7 @@ wheels = [ [[package]] name = "jupytext" -version = "1.19.1" +version = "1.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", marker = "sys_platform == 'linux'" }, @@ -2524,9 +2528,9 @@ dependencies = [ { name = "packaging", marker = "sys_platform == 'linux'" }, { name = "pyyaml", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/a5/80c02f307c8ce863cb33e27daf049315e9d96979e14eead700923b5ec9cc/jupytext-1.19.1.tar.gz", hash = "sha256:82587c07e299173c70ed5e8ec7e75183edf1be289ed518bab49ad0d4e3d5f433", size = 4307829, upload-time = "2026-01-25T21:35:13.276Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/4f13fcba0ed05965a48fca197d89fb8c78c4b61051dc0c9ee9ed92e77a8d/jupytext-1.19.2.tar.gz", hash = "sha256:da6198a42406a09142b6b26ebc46a3ec7077f525222a8f12b1811a0e289a2216", size = 4309931, upload-time = "2026-05-10T17:10:40.345Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl", hash = "sha256:d8975035155d034bdfde5c0c37891425314b7ea8d3a6c4b5d18c294348714cd9", size = 170478, upload-time = "2026-01-25T21:35:11.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/b4b86e5fa07543bfbbcdc6c9f7f9f561e66a5f3539992e3009973f2b1314/jupytext-1.19.2-py3-none-any.whl", hash = "sha256:8a31e896c7e9215841783aade24336e945543057e1c2d7f00b22f9e870348688", size = 170653, upload-time = "2026-05-10T17:10:38.418Z" }, ] [[package]] @@ -2862,7 +2866,7 @@ hilserl = [ { name = "placo" }, { name = "protobuf" }, { name = "pyarrow" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "torchcodec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'win32'" }, { name = "transformers" }, ] hopejr = [ @@ -3227,62 +3231,62 @@ provides-extras = ["dataset", "training", "hardware", "viz", "core-scripts", "ev [[package]] name = "librt" -version = "0.10.0" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/cb/c1945e506893b5b8577fb45a60c80e3ffe4a82092a04a6f29b0b951d9a24/librt-0.10.0.tar.gz", hash = "sha256:1aba1e8aa4e3307a7be68a74149545fde7451964dc0235a8bec5704a17bdda42", size = 191799, upload-time = "2026-05-05T16:31:23.535Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/8e/cbb5b6f6e45e65c10a42449a69eaccc44d73e6a081ea752fbc5221c6dc1c/librt-0.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b4b58a44b407e91f633dafee008de9ddea6aa2a555ed94929c099260910bd0ba", size = 77327, upload-time = "2026-05-05T16:29:38.919Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3d/8233cbee8e99e6a8992f02bfc2dec8d787509566a511d1fde2574ee7473f/librt-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:950b79b11762531bdf45a9df909d2f9a2a8445c70c88665c01d14c8511a27dc5", size = 79971, upload-time = "2026-05-05T16:29:40.96Z" }, - { url = "https://files.pythonhosted.org/packages/87/6f/5264b298cef2b72fc97d2dde56c66181eda35204bf5dcd1ed0c3d0a0a782/librt-0.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4538453f51be197633b425912c150e25b0667252d3741c53e8368176d98d9d37", size = 246559, upload-time = "2026-05-05T16:29:42.701Z" }, - { url = "https://files.pythonhosted.org/packages/07/7b/19b1b859cc60d5f99276cc2b3144d91556c6d1b1e4ebb50359696bebf7a8/librt-0.10.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:70b955f091beac93e994a0b7ec616934f63b3ea5c3d6d7af847562f935aceca7", size = 235216, upload-time = "2026-05-05T16:29:44.193Z" }, - { url = "https://files.pythonhosted.org/packages/6e/56/a2f40717142a8af46289f57874ef914353d8faccd5e4f8e594ab1e16e8c7/librt-0.10.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:483e685e06b6163728ba6c85d74315176be7190f432ec2a41226e5e14355d5f0", size = 263108, upload-time = "2026-05-05T16:29:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/67/ca/15c625c3bdc0167c01e04ef8878317e9713f3bfa788438342f7a94c7b22c/librt-0.10.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ac53d946a009d1a38c44a60812708c9458fb2a239a5f630d8e625571386650f", size = 255280, upload-time = "2026-05-05T16:29:48.087Z" }, - { url = "https://files.pythonhosted.org/packages/ed/c5/ba301d571d9e05844e2435b73aba30bee77bb75ce155c9affcfd2173dd03/librt-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc8771c9fcf0ea894ca41fdc2abd83572c2fbda221f232d86e718614e57ff513", size = 268829, upload-time = "2026-05-05T16:29:49.628Z" }, - { url = "https://files.pythonhosted.org/packages/8b/60/af70e135bc1f1fe15dd3894b1e4bbefc7ecdf911749a925a39eb86ceb2a1/librt-0.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:70805dbc5257892ac572f86290a61e3c8d90224ecce1a8b2d1f7ed51965417f4", size = 262051, upload-time = "2026-05-05T16:29:51.244Z" }, - { url = "https://files.pythonhosted.org/packages/83/c2/c8236eb8b421bac5a172ba208f965abaa89805da2a3fa112bdf1764caf8f/librt-0.10.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d3b4f300f7bcba6e2ff73fb8bef1898479e9772bfa2682998c636391633ec826", size = 264347, upload-time = "2026-05-05T16:29:53.013Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/15b6d32bc25dacd4a60886a683d8128d6219910c122202b995a40dd4f8d2/librt-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:943bc943f92f4fb3408fae62485c6a3ad68ce4f2ee205643a39641525c19a276", size = 286482, upload-time = "2026-05-05T16:29:54.675Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8e/b1b959bacd323eb4360579db992513e1406d1c6ef7edb57b5511fd0666fd/librt-0.10.0-cp312-cp312-win32.whl", hash = "sha256:6065c1a758fba1010b41401013903d3d5d2750eab425ddedd584abac31d0630e", size = 62955, upload-time = "2026-05-05T16:29:56.39Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4c/d4cd6e4b9fc24098e63cc85537d1b6689682aee96809c38f08072067cc2b/librt-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:d788ecbe208ab352dab0e105cc06057bf9a2fc7e58cabb0d751ad9e30062b9e2", size = 71191, upload-time = "2026-05-05T16:29:57.682Z" }, - { url = "https://files.pythonhosted.org/packages/2b/19/8641da1f63d24b92354a492f893c022d6b3a0df44e70c8eff49364613983/librt-0.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:6003d1f295bdba02656dc81308208fc060d0a51d8c0d0a6db70f7f3c57b9ba0a", size = 61432, upload-time = "2026-05-05T16:29:58.971Z" }, - { url = "https://files.pythonhosted.org/packages/e5/29/681a75c82f4cc90d29e4b257a3299b79fe13fe927a04c57b8109d70b6957/librt-0.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0ede79d682e73f91c1b599a76d78b7464b9b5d213754cedb13372d9df36e596", size = 77299, upload-time = "2026-05-05T16:30:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/62/24/0c7ca445a55d04be79cac19819437fd094782347fa116f6681844fa6143e/librt-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0ba0b131fdb336c8b9c948e397f4a7e649d0f783b529f07b647bf4961df392e", size = 79930, upload-time = "2026-05-05T16:30:01.555Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1f/1e2b8f6443ef9e9a81e89486ca70e22f3684f93db003ce6eaefc3d0839b9/librt-0.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2728117da2afb96fb957768725ee43dc9a2d73b031e02da424b818a3cdd3a275", size = 246195, upload-time = "2026-05-05T16:30:03.261Z" }, - { url = "https://files.pythonhosted.org/packages/74/61/9dc9e03de0439ad84c1c240aac8b747f12c90cb797ea6042f7bdb8d3410f/librt-0.10.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:723ba80594c49cdf0584196fc430752262605dc9449902fc9bd3d9b79976cb77", size = 234951, upload-time = "2026-05-05T16:30:04.881Z" }, - { url = "https://files.pythonhosted.org/packages/55/f4/635223117d7590875bca441275065a3bf491203ad4208bd1cc3ffd90c5a1/librt-0.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7292edaaca294a61a978c53a3c7d6130d099b0dfbc8f0a65916cdc6b891b9852", size = 262768, upload-time = "2026-05-05T16:30:06.638Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/b04152d0cd8b6ca2b428a8bd3230343230c35ed304a932f35b5375f2f828/librt-0.10.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89fe9d539f2c10a1666633eeeac507ce95dd06d9ecc58de3c6390dba156a3d3a", size = 255075, upload-time = "2026-05-05T16:30:08.216Z" }, - { url = "https://files.pythonhosted.org/packages/35/1e/25bac4c7f2ca36f0e612cade186970683cf79153d96beccc3a11a9e19b97/librt-0.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4efa7b9587503fa5b67f40593302b9c8836d211d222ff9f7cafe67be5f8f0b10", size = 268559, upload-time = "2026-05-05T16:30:10.1Z" }, - { url = "https://files.pythonhosted.org/packages/18/54/4601faab35b6632a13200faa146ca62bfd111ffbe2568be430d65c89493a/librt-0.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:22dc982ef59df0136df36092ccbdbb570ced8aafb33e49585739b2f1de1c13b6", size = 261753, upload-time = "2026-05-05T16:30:11.912Z" }, - { url = "https://files.pythonhosted.org/packages/1b/cf/39f4023509e94fade8b074666fa3292db9cb6b34ea5dcbe7af53df9fca1d/librt-0.10.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6f2e5f3606253a84cea719c94a3bb1c54487b5d617d0254d46e0920d8a06be3f", size = 264055, upload-time = "2026-05-05T16:30:13.465Z" }, - { url = "https://files.pythonhosted.org/packages/8e/00/40247209fc46a8e308a91412d5206aedf8efb667ee89eb625820106a5c2f/librt-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40884bfaa1e29f6b6a9be255007d8f359bfc9e61d68bdef8ed3158bfcbc95df9", size = 286190, upload-time = "2026-05-05T16:30:15.073Z" }, - { url = "https://files.pythonhosted.org/packages/d8/6e/5566beb94431a985abe1787af5ef86e087750172ff9d0bbf20f93e88132d/librt-0.10.0-cp313-cp313-win32.whl", hash = "sha256:3cd34cd8254eba756660bff6c2da91278248184301054fe3e4feb073bdd49b14", size = 62949, upload-time = "2026-05-05T16:30:16.503Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c2/3ea3301d6c8dff51d39dbe8ed75db3dc92896947d4afb5eeadf821c1e67f/librt-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:7baac5313e2d8dce1386f97777a8d03ab28f5fe1e780b3b9ac2ee7544551fedc", size = 71152, upload-time = "2026-05-05T16:30:17.766Z" }, - { url = "https://files.pythonhosted.org/packages/3c/de/5d49cb92cadcbc77d3abc27b93fd6030ed8437487dde2eae38cab5e6704d/librt-0.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:afc5b4406c8e2515698d922a5c7823a009312835ea58196671fff40e35cb8166", size = 61336, upload-time = "2026-05-05T16:30:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/6a/64/7165e08108cc185a13a9c069f0685e6ef92e70e07fddf7edf5e7348c6316/librt-0.10.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f09588a30e6a22ec624090d72a3ab1a6d4d5485c3ed739603e76aa3c16efa688", size = 76794, upload-time = "2026-05-05T16:30:20.392Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ef/bf8613febf651b90c5222ee79dea5ae58d4cc2b544df69d3033424448934/librt-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:131ade118d12bd7a0adc4e655474a553f1b76cf78385868885944d21d51e45e0", size = 79662, upload-time = "2026-05-05T16:30:22.025Z" }, - { url = "https://files.pythonhosted.org/packages/b6/67/9eddd165c1d8397bdf99b38bf12b5a55b3def5035b49eedb49f2775d1430/librt-0.10.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8b9ab28e40d011c373a189eae900c916e66d6fbecf7983e9e4883089ee085ef", size = 242390, upload-time = "2026-05-05T16:30:23.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/d1/d95da80334501866cd37004ab5d7483220d05862fab4b5405394f0264f0d/librt-0.10.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:67c39bb30da73bae1f293d1ed8bc2f8f6642649dd0928d3600aeff3041ac23d6", size = 232603, upload-time = "2026-05-05T16:30:25.198Z" }, - { url = "https://files.pythonhosted.org/packages/0c/fa/e6d64d28718bc1be4e1736fcb037ca1c4dfca927e7167df75a7d5215665e/librt-0.10.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c3273c6b774614f093c8927c2bf1b077d0fefde988fe98f46a333734e5597ab", size = 259187, upload-time = "2026-05-05T16:30:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/72/3f/3fdb77e7f937dad59cfd76b720be7e7643400ec76b2da35befab8d66ba30/librt-0.10.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9dd7c1b86a4baa583ab5db977484b93a2c474e69e96ef3e9538387ea54229cb9", size = 251846, upload-time = "2026-05-05T16:30:28.56Z" }, - { url = "https://files.pythonhosted.org/packages/18/ca/f4d49133dd86a6f55d79eca30bf412fa722f511a9abe67f62f57aa64e66a/librt-0.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a77385c5a202e831149f7ad03be9e67cf80e957e52c614e83dcb822c95222eb8", size = 264936, upload-time = "2026-05-05T16:30:30.491Z" }, - { url = "https://files.pythonhosted.org/packages/de/66/a8df2fbadc1f6c1827a096d11c40175bd526133480bd3bc88ec64a03d257/librt-0.10.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c6a5eafa74b5655bad59886138ed68426f098a6beb8cb95a71f2cc3cd8bb33fe", size = 258699, upload-time = "2026-05-05T16:30:32.002Z" }, - { url = "https://files.pythonhosted.org/packages/bb/73/1e3c83613fe05451bb969e27b68a573d177f08d5f63533cc29fec0989658/librt-0.10.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1fc93d0439204c50ab4d1512611ce2c206f1b369b419f69c7c27c761561e3291", size = 259825, upload-time = "2026-05-05T16:30:35.077Z" }, - { url = "https://files.pythonhosted.org/packages/09/24/5e2f926ee9d3ef348d9339526d7062abb5c44d8419e3179528c01d78c102/librt-0.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:79e713c178bc7a744adfbee6b4619a288eecc0c914da2a9313a20255abe2f0cf", size = 282548, upload-time = "2026-05-05T16:30:36.639Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7d/3e89ed6ad0162561fa8bef9df3195e24263104c955713cd0237d3711fad2/librt-0.10.0-cp314-cp314-win32.whl", hash = "sha256:2eba9d955a68c41d9f326be3da42f163ec3518b7ab20f1c826224e7bed71e0bf", size = 58970, upload-time = "2026-05-05T16:30:38.183Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/579e731c94a7086a268bfa3e7a4945cd47836bebd3cbf3faeafd2e7eaef9/librt-0.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbfaf7f5145e9917f5d18bffa298eff6a19d74e7b8b11dabdca95785befe8dbf", size = 67260, upload-time = "2026-05-05T16:30:39.804Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f8/235822b7ae0b2334f12ee18bcf2476d07924077a5efeea57dbe927704be2/librt-0.10.0-cp314-cp314-win_arm64.whl", hash = "sha256:8d6d385d1969849a6b1397114df22714b6ded917bada98668e3e974dc663477e", size = 57156, upload-time = "2026-05-05T16:30:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e3/9b919cbf1e8eb770bf91bb7df28125e0f1daf4587169afefd95402636e9a/librt-0.10.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:6c3a82d3bd32631ef5c79922dfc028520c9ad840255979ab4d908271818039ee", size = 79150, upload-time = "2026-05-05T16:30:42.761Z" }, - { url = "https://files.pythonhosted.org/packages/6a/f5/72a944aa3bc3498169a168087eff58ca48b58bf1b704e59d091fd30739f3/librt-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d64cc66005dc324c9bb1fa3fc2841f529002f6eb15966d55e46d430f56955a6a", size = 82304, upload-time = "2026-05-05T16:30:44.082Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e3/fcc290a33e295019759472dfa794d204e43504b276ac65eab7fd9da20ea3/librt-0.10.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb562cd28c88cd2c6a9a6c78f99dc39348d6b16c94adc25de0e574acf1176e9", size = 272556, upload-time = "2026-05-05T16:30:45.497Z" }, - { url = "https://files.pythonhosted.org/packages/fd/54/546975e4c997573885e7f040a05012f8838e06fb12b0c3c1fbb76254e9d7/librt-0.10.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b809aa2854d019c28773b03605df22adc675ee4f3f4402d673581313e8906119", size = 256941, upload-time = "2026-05-05T16:30:47.059Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f1d03401571b331653acddbd4e8cd955c06d945241dd08b25192fac0d04b/librt-0.10.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc15acabdd519bd4176fdadc2119e5e3093485d86f89138daf47e5b4cedb983a", size = 285855, upload-time = "2026-05-05T16:30:48.86Z" }, - { url = "https://files.pythonhosted.org/packages/0c/08/62cf80ff046c339faf56718b3a940244d4beb70f1c6407289b5830ec11e9/librt-0.10.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b1b2d835307d08ddadd94568e2369648ec9173bd3eea6d7f52a1abe717c81f98", size = 275321, upload-time = "2026-05-05T16:30:50.63Z" }, - { url = "https://files.pythonhosted.org/packages/d9/ea/da5918d4070362e9a4d2ee9cd34f9dc84902daad8fd4275f8504a727ff4e/librt-0.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d261c6a2f93335a5167887fb0223e8b98ffce20ee3fde242e8e58a37ece6d0e5", size = 293993, upload-time = "2026-05-05T16:30:52.577Z" }, - { url = "https://files.pythonhosted.org/packages/c9/8d/68b6086bed1fcdc314c640ea04e31e52d18052e08059fa595409d66a51a9/librt-0.10.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e2ffd44963f8e7f68995504d90f9881d64e94dc1d8e310039b9526108fc0c0f7", size = 284254, upload-time = "2026-05-05T16:30:55.086Z" }, - { url = "https://files.pythonhosted.org/packages/06/c8/b810f1d84ec34a5a7ed93d7b510ab04164d75fbdf23088d5c3fbe6b08357/librt-0.10.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f285f6455ed495791c4d8630e5af732960adea93cac4c893d15619f2eae53e8", size = 284925, upload-time = "2026-05-05T16:30:56.728Z" }, - { url = "https://files.pythonhosted.org/packages/5a/00/3c82d4158c5a2c62528b8fccce65a8c9ad700e480e86f9389387435089a5/librt-0.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6034ff52e663d34c7b82ef2aa2f94ad7c1d939e2368e63b06844bc4d127d2e1", size = 307830, upload-time = "2026-05-05T16:30:58.377Z" }, - { url = "https://files.pythonhosted.org/packages/99/3a/9c635ac3e8a00383ff689161d3eac8a30b3b2ddc711b40471e6b8983ea29/librt-0.10.0-cp314-cp314t-win32.whl", hash = "sha256:657860fd877fba6a241ea088ef99f63ca819945d3c715265da670bad56c37ebe", size = 60147, upload-time = "2026-05-05T16:31:00.293Z" }, - { url = "https://files.pythonhosted.org/packages/dc/e8/6f65f3e565d4ac212cddddd552eacc8035ffdf941ca0ad6fe945a211d41f/librt-0.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56ded2d66010203a0cb5af063b609e3f079531a0e5e576d618dece859fd2e1af", size = 68649, upload-time = "2026-05-05T16:31:01.778Z" }, - { url = "https://files.pythonhosted.org/packages/51/78/a0705a67cacd81e5fa01a5035b3adbdfbb43a7b8d4bd27e2b282ae61baf2/librt-0.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1ee63f30abf18ed4830fdbaf87b2b6f4bba1e198d46085c314edde4045e56715", size = 58247, upload-time = "2026-05-05T16:31:03.191Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, ] [[package]] @@ -3660,7 +3664,7 @@ wheels = [ [[package]] name = "mujoco" -version = "3.8.0" +version = "3.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "absl-py" }, @@ -3669,23 +3673,23 @@ dependencies = [ { name = "numpy" }, { name = "pyopengl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/d8/9aae1a021b6e15ee69d805d893e01dda71cbaae1c75d5f8ec8e12916cb7c/mujoco-3.8.0.tar.gz", hash = "sha256:250afe57458d6881b2d7659fa0029a128cb57cbbb620268d95647fb9ad742183", size = 918250, upload-time = "2026-04-24T22:59:07.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/ba/ad135f7a4a71360072bc4f202f7ab3130d7d7827cff70ce3e1b382a1a410/mujoco-3.8.1.tar.gz", hash = "sha256:019a0b3406892bc98454eaf55cbd27f85d167758ad785f77c608a61f3a34ad17", size = 922269, upload-time = "2026-05-11T13:44:01.357Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/0d/35aad24bef1f36e9ebf63367938b16abec82407338d612c37624ff20b0e3/mujoco-3.8.0-cp312-cp312-macosx_10_16_x86_64.whl", hash = "sha256:a495da0cd01aff6ac94ec97f0a1d913e1afe071daf107e220f81814435227982", size = 7265096, upload-time = "2026-04-24T22:58:33.475Z" }, - { url = "https://files.pythonhosted.org/packages/60/d7/2ee5a123431eb50f234de2759e46f3d0c02876e0b1ffce1b26102ed388e7/mujoco-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cc1d25b0cd47248fd39681310950b2bea0f6098f57358c0c02730d365bb80ba1", size = 7204862, upload-time = "2026-04-24T22:58:35.978Z" }, - { url = "https://files.pythonhosted.org/packages/aa/62/a488e6e0963e0210b8262650d25e51c4c597ff7beed4fe01a7e88e3abfc5/mujoco-3.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:980ab5a2210777cf766e53eb574726f9360e2a87e47d83a6c8d801fb71f2fe52", size = 6743542, upload-time = "2026-04-24T22:58:38.137Z" }, - { url = "https://files.pythonhosted.org/packages/6f/de/bc2271210dad5c6ab73af294779226308e9cf4ed8bc2dbe59922eb8702ed/mujoco-3.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:323fedd14905b73cfe56ea8ff916716ccf8b57cff348a7aa6932c8983a465d64", size = 7226045, upload-time = "2026-04-24T22:58:41.117Z" }, - { url = "https://files.pythonhosted.org/packages/65/87/198a88747ff0c01e35070c0c80ae0c05ff8d1a61d6e6f379a4e5ff3e6185/mujoco-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:8db22dbc5a6c98241549c8161f20a2b0c2ccc5d08fa42595e7a4b594e35a70dd", size = 5813167, upload-time = "2026-04-24T22:58:43.469Z" }, - { url = "https://files.pythonhosted.org/packages/11/7d/41c73ebe93565ed196ec5ad012232138e3d10850e841ccd77d459afc4383/mujoco-3.8.0-cp313-cp313-macosx_10_16_x86_64.whl", hash = "sha256:d4a080aab0be4d02162e6fe3bcd7163c01cc751638f5a84ba05477b512d95cc0", size = 7265494, upload-time = "2026-04-24T22:58:45.119Z" }, - { url = "https://files.pythonhosted.org/packages/7c/05/d21b43c31c5d9179c2d33e0d38896775b262a9d78729b760717927a02e28/mujoco-3.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5aa987e70a6601ebf02d123d9842f2d1b8f8057163feec1f0a5a049de1cbe252", size = 7205049, upload-time = "2026-04-24T22:58:46.874Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9c/af181776d0ffb70ad6a4365f0613529f268782850dedab0569c6cce83fcc/mujoco-3.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8bd33b7e2382605012dfe41c00a8d3bb358153e5b019d920a52c31664472ce20", size = 6743578, upload-time = "2026-04-24T22:58:48.862Z" }, - { url = "https://files.pythonhosted.org/packages/89/8a/c9b28784a7e51926d609b70842e16b85e286df87ad861dbbb26c4e49cacf/mujoco-3.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2b3de0c9fed950c5080ea4b3ff1fb5c89f88e22798f1e1693ec8dbbd36de00b", size = 7226464, upload-time = "2026-04-24T22:58:51.039Z" }, - { url = "https://files.pythonhosted.org/packages/50/3f/0a72c74dd766524b9f1b79f0d6d327b9a797d87b44fe62b3068b44123b54/mujoco-3.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:de03d173f4d9c7341b5dcc10a8eddb36bb19989df68f24369dc7c782dc053f11", size = 5813265, upload-time = "2026-04-24T22:58:53.316Z" }, - { url = "https://files.pythonhosted.org/packages/81/f7/afdbcb4ad50786ed7500205f29ffb5c3a5ef9d42e6b3ad8f9636c4911687/mujoco-3.8.0-cp314-cp314-macosx_10_16_x86_64.whl", hash = "sha256:09c27fc6ce1560912e920789bc121290e4c84919ae30f7b54da5efed4cd2804a", size = 7320672, upload-time = "2026-04-24T22:58:55.469Z" }, - { url = "https://files.pythonhosted.org/packages/4a/8a/6299f6209084dda9469374461c77adad5d63041427b9b9bd4fadaf0c35b0/mujoco-3.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5fc4e930cd1414f965381ac97ec054a001539e7aa462836145f6a3201b0dfe88", size = 7249791, upload-time = "2026-04-24T22:58:57.126Z" }, - { url = "https://files.pythonhosted.org/packages/df/3c/a74d169b7725aee971962238d2aee767f64496ede1367cd558361c97d5ca/mujoco-3.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14c32992267906d422ed2127e99aa9ad036a62324139da2a3bd25df1e928d0ee", size = 6754009, upload-time = "2026-04-24T22:59:00.375Z" }, - { url = "https://files.pythonhosted.org/packages/66/36/f3610724bb35f6cfb2ffebe9d8d315975e5fa9722146ad58298df442da7e/mujoco-3.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1373c4744a3424f1aa224d2ad5201497ea150c4698beb9aaeb8c3560efa60fdb", size = 7227764, upload-time = "2026-04-24T22:59:02.722Z" }, - { url = "https://files.pythonhosted.org/packages/92/5a/80f5347c322300e4402b08df74e7489756e9af060bb8b8d342086dd5b41c/mujoco-3.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:f8da8fc4a2861f9d1eef64d83adcd783bfe5c02bdc78af2d963d942d097dfdce", size = 6144239, upload-time = "2026-04-24T22:59:05.329Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4f/ccfaf99fab0042bef61da2cd21d47f41fcff069744b42bc6829d9da6b4fc/mujoco-3.8.1-cp312-cp312-macosx_10_16_x86_64.whl", hash = "sha256:b1c5213eb8fc53ee8c0713391eda5f0766088766025e8b46e1dfd3cb9f00db71", size = 7356297, upload-time = "2026-05-11T13:43:30.654Z" }, + { url = "https://files.pythonhosted.org/packages/ec/33/64e4de2ff6f0ead6c9d204efdd68aea3ca235058829f69b957625768495d/mujoco-3.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06e36128883f484e173d02e6140ae09c1ff9845dbc7fb605b02361d9d2ecaccf", size = 7245562, upload-time = "2026-05-11T13:43:32.736Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/597263af3519b1ac1c09bb8fc8f8985e90e4990c8aff62ca1a0c035e824c/mujoco-3.8.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e4e951de3ab2ba5187b4807c648ac551fd6ae27ff04c7e25304b357d35d7fe8", size = 6784316, upload-time = "2026-05-11T13:43:34.837Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2f/8a911be3ed84436bc46b5bdcf4ead6ac9590d0ddc82c006834c49ffd60c4/mujoco-3.8.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2970c90913a52b31eade56f27e5439905f08f1479e7991e257144ca829fa1267", size = 7322546, upload-time = "2026-05-11T13:43:37.074Z" }, + { url = "https://files.pythonhosted.org/packages/83/58/d580c4abf7dda5231fed0886a626e1815e40c2d118bf7d0052a617435d6e/mujoco-3.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:496b61c863d544e076990d1601555f71e522eb3b7aaae227dfbc5dea522f88a1", size = 5893327, upload-time = "2026-05-11T13:43:39.022Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/55222063035a96748e78fcd690deb5ffbb4f000bbb24eec8220173f6101c/mujoco-3.8.1-cp313-cp313-macosx_10_16_x86_64.whl", hash = "sha256:1728de8572674cb146e134e96ecee8ed2c2f5e01d4aacacf42d703a441cfcea5", size = 7356742, upload-time = "2026-05-11T13:43:41.121Z" }, + { url = "https://files.pythonhosted.org/packages/1a/97/636a41e34f9a9bcae6c59e8d884b12762b6bc2a912e95a40e1e3e02884f2/mujoco-3.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96811f3903f208b3facca4cb7be98207e4b481cafcc1c3082b1184c28f778665", size = 7245661, upload-time = "2026-05-11T13:43:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0c/d8c164aef4a2d51ecb23a81db1ebac9e7da99233e6207ea4801b507ebc92/mujoco-3.8.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64076a564455c68550e3bc1be4788e0e121c6e38db014eee39b1e1d7a733415", size = 6784309, upload-time = "2026-05-11T13:43:44.771Z" }, + { url = "https://files.pythonhosted.org/packages/f4/76/5b9d2a223d91c66a5ddf1aad2293c33ac795cabe8de0fa292550c28db374/mujoco-3.8.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad3cfa1c809bf70a6308aa1db7b1e44d019f7a499c0b0ac8a4ddff102f4fae54", size = 7322830, upload-time = "2026-05-11T13:43:46.859Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e8/e0432f14495d561aa0d1c77f05096a8c8a91f934b703a324c1abda0e57ba/mujoco-3.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:9010b5929c2ad924537a528b0299c2dc552cdbb136cea6da24544793590c2487", size = 5893667, upload-time = "2026-05-11T13:43:48.987Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/757e83c9f8291e8ea84105cd76de076f674e120d98b6cd21716504d7bfec/mujoco-3.8.1-cp314-cp314-macosx_10_16_x86_64.whl", hash = "sha256:5717c8cdfe0360a42f8f65af53b2f5e3c38318f0c4ace181b81583db4e8fbc8f", size = 7412564, upload-time = "2026-05-11T13:43:51.538Z" }, + { url = "https://files.pythonhosted.org/packages/27/cc/1feae64fb8dc40d2ebb5b8938a13acd3ba5dfb9acf49c3e5907d9d42faf8/mujoco-3.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7a311b25c8284729505d1dc130501633ee2b00249d0102009131a4b23f0ce2f", size = 7295087, upload-time = "2026-05-11T13:43:53.848Z" }, + { url = "https://files.pythonhosted.org/packages/85/7c/ca2fc8f4d0054eeb7dafe7ed047bda47661f077c22dc09e6350e46a576fc/mujoco-3.8.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:804c66272e6ffd2223c7c69d020dc21e35986d2c633b20ec92087f34acfb40e2", size = 6794464, upload-time = "2026-05-11T13:43:56.357Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9a/88e689fa5e21ff6e1fcd870429d33eb0f2ef58d5330db2ca0e6bea91b7d2/mujoco-3.8.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2d4fcdb57b4ea7d8730db3d995e900f5c6281fafbd552161c363e6ff9e839959", size = 7324034, upload-time = "2026-05-11T13:43:58.091Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/8542d94dd3d73af37befc6b646c927f8448d851a61a5b9814371e581e0fb/mujoco-3.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:e32717426b59e2619b5a44253856978a1312f12912e6404907acb551df3da4ff", size = 6227305, upload-time = "2026-05-11T13:43:59.932Z" }, ] [[package]] @@ -3806,7 +3810,7 @@ wheels = [ [[package]] name = "mypy" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ast-serialize" }, @@ -3815,37 +3819,37 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/dc/7e6d49f04fca40b9dd5c752a51a432ffe67fb45200702bc9eee0cb4bbb26/mypy-2.0.0.tar.gz", hash = "sha256:1a9e3900ac5c40f1fe813506c7739da6e6f0eab2729067ebd94bfb0bbba53532", size = 3869036, upload-time = "2026-05-06T19:26:43.22Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/4b/f6cd12ef1eb63be1c342da3e8ca811d2280276177f6de4ef20cb2366d79b/mypy-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:660790551c988e69d8bf7a35c8b4149edeb22f4a339165702be843532e9dcdb5", size = 14756610, upload-time = "2026-05-06T19:26:19.221Z" }, - { url = "https://files.pythonhosted.org/packages/32/73/67d09ca28bee21feaca264b2a680cf2d300bcc2071136ad064928324c843/mypy-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7a15bf92cd8781f8e72f69ffa7e30d1f434402d065ee1ecd5223ef2ef100f914", size = 13554270, upload-time = "2026-05-06T19:26:08.977Z" }, - { url = "https://files.pythonhosted.org/packages/61/b3/44718b5c6b1b5a27440ff2effe6a1be0fa2a190c0f4e2e21a83728416f95/mypy-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ff370b43d7def05bbcd2f5267f0bcda72dd6a552ef2ea9375b02d6fe06da270", size = 13924663, upload-time = "2026-05-06T19:21:24.932Z" }, - { url = "https://files.pythonhosted.org/packages/6a/2b/bbb9cc5773f946846a7c340097e59bcf84095437dda0d56bb4f6cf1f6541/mypy-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37bd246590a018e5a11703b7b09c39d47ede3df5ba3fa863c5b8590b465beb01", size = 14946862, upload-time = "2026-05-06T19:24:23.023Z" }, - { url = "https://files.pythonhosted.org/packages/43/25/e9318566f443a5130b4ff0ad3367ee6c4c4c49ff083fe5214a7318c18282/mypy-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cce87e92214fac8bf8feb8a680d0c1b6fb748d50e9b57fbb13e4b1d83a3ed19b", size = 15175090, upload-time = "2026-05-06T19:26:28.794Z" }, - { url = "https://files.pythonhosted.org/packages/67/65/2ec28c834f21e164c33bc296a7db538ad50c74f83e517c7a0be95ff6de86/mypy-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e19e9cb69b66a4141009d24898259914fa2b71d026de0b46edf9fafdbf4fd46e", size = 11052899, upload-time = "2026-05-06T19:25:39.084Z" }, - { url = "https://files.pythonhosted.org/packages/9e/72/d1ec625cfc9bd101c07a6834ef1f94e820296f8fdbad2eb03f50e0983f8c/mypy-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:b021614cb08d44785b025982163ec3c39c94bff766ead071fa9e82b4ef6f62cd", size = 9972935, upload-time = "2026-05-06T19:23:24.204Z" }, - { url = "https://files.pythonhosted.org/packages/e5/c6/996a1e535e5d0d597c3b1460fc962733091f885f312e749350eb2ac10965/mypy-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ef5f581b61240d1cc629b12f8df6565ed6ffac0d82ed745eef7833222ab50b9", size = 14737259, upload-time = "2026-05-06T19:20:23.081Z" }, - { url = "https://files.pythonhosted.org/packages/94/c5/0f9460e26b77f434bd53f47d1ce32a3cd4580c92a5331fa5dfc059f9421a/mypy-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20e3470a165dbc249bdfbe8d1c5172727ef22688cffc279f8c3aa264ab9d4d9a", size = 13538377, upload-time = "2026-05-06T19:21:08.804Z" }, - { url = "https://files.pythonhosted.org/packages/b2/3e/8ea2f8dd1e5c9c279fb3c28193bdb850adf4d3d8172880abad829eced609/mypy-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:224ba142eee8b4d65d4db657cb1fc22abec30b135ded6ab297302ba1f62e505d", size = 13914264, upload-time = "2026-05-06T19:24:12.875Z" }, - { url = "https://files.pythonhosted.org/packages/be/ce/78bd3b8520f676acee9dab48ea71473e68f6d5cf14b59fbd800bea50a92b/mypy-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e879ad8a03908ff74d15e8a9b42bf049918e6798d52c011011f1873d0b5877e", size = 14926761, upload-time = "2026-05-06T19:20:12.846Z" }, - { url = "https://files.pythonhosted.org/packages/61/ef/b52fa340522da3d22e669117c3b83155c2660f7cdc035856958fbfffb224/mypy-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65c5c15bcbd18d6fe927cc55c459597a3517d69cc3123f067be3b020010e115e", size = 15157014, upload-time = "2026-05-06T19:25:49.78Z" }, - { url = "https://files.pythonhosted.org/packages/7a/0c/dde7614250c6d017936c7aa3bb63b9b52c7cfd298d3f1be9be45f307870b/mypy-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:d1a068acd7c9fb77e9f8923f1556f2f49d6d7895821121b8d97fa5642b9c52f5", size = 11067049, upload-time = "2026-05-06T19:21:16.116Z" }, - { url = "https://files.pythonhosted.org/packages/27/ec/1d6af4830a94a285442db19caa02f160cc1a255e4f324eec5458e6c2bafb/mypy-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:ef9d96da1ddffbc21f27d3939319b6846d12393baa17c4d2f3e81e040e73ce2c", size = 9967903, upload-time = "2026-05-06T19:22:15.52Z" }, - { url = "https://files.pythonhosted.org/packages/ce/2c/6fefe954207860aed6eeb91776795e64a257d3ce0360862288984ce121f5/mypy-2.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c918c64e8ce36557851b0347f84eb12f1965d3a06813c36df253eb0c0afd1d82", size = 14729633, upload-time = "2026-05-06T19:24:53.383Z" }, - { url = "https://files.pythonhosted.org/packages/23/d6/d336f5b820af189eb0390cce21de62d264c0a4e64713dfbe81bfc4fc7739/mypy-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:301f1a8ccc7d79b542ee218b28bb49443a83e194eb3d10da63ff1649e5aa5d34", size = 13559524, upload-time = "2026-05-06T19:22:24.906Z" }, - { url = "https://files.pythonhosted.org/packages/af/a6/d7bb54fde1770f0484e5fbdbdce37a41e95ed0a1cd493ec60ead111e356c/mypy-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdf4ef489d44ce350bac3fd699907834e551d4c934e9cc862ef201215ab1558d", size = 13936018, upload-time = "2026-05-06T19:25:02.992Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ba/5be51316b91e6a6bf6e3a8adb3de500e7e1fb5bf9491743b8cbc81a34a2c/mypy-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cde2d0989f912fc850890f727d0d76495e7a6c5bdd9912a1efdb64952b4398d", size = 14910712, upload-time = "2026-05-06T19:25:21.83Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/e2c8c3b373e20ebfb66e6c83a99027fd67df4ec43b08879f74e822d2dc4c/mypy-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdf05693c231a14fe37dbfce192a3a1372c26a833af4a80f550547742952e719", size = 15141499, upload-time = "2026-05-06T19:20:50.924Z" }, - { url = "https://files.pythonhosted.org/packages/12/36/07756f933e00416d912e35878cfcf89a593a3350a885691c0bb85ae0226a/mypy-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:73aee2da33a2237e66cbe84a94780e53599847e86bb3aa7b93e405e8cd9905f2", size = 11240511, upload-time = "2026-05-06T19:21:32.39Z" }, - { url = "https://files.pythonhosted.org/packages/70/05/79ac1f20f2397353f3845f7b8bb5d8006cda7c8ef9092f04f9de3c6135f2/mypy-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:1f6dcd8f39971f41edab2728c877c4ac8b50ad3c387ff2770423b79a05d23910", size = 10149336, upload-time = "2026-05-06T19:22:08.383Z" }, - { url = "https://files.pythonhosted.org/packages/53/e0/0db84e0ebbad6e99e566c68e4b465784f2a2294f7719e8db9d509ef23087/mypy-2.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a04e980b9275c76159da66c6e1723c7798306f9802b31bdaf9358d0c84030ce8", size = 15797362, upload-time = "2026-05-06T19:22:00.835Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a4/14cc0768164dd53bec48aa41a20270b18df9bf72aa5054278bf133608315/mypy-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:33f9cf4825469b2bc73c53ba55f6d9a9b4cdb60f9e6e228745581520f29b8771", size = 14635914, upload-time = "2026-05-06T19:23:43.675Z" }, - { url = "https://files.pythonhosted.org/packages/08/48/d866a3e23b4dc5974c77d9cf65a435bf22de01a84dd4620917950e233960/mypy-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191675c3c7dc2a5c7722a035a6909c277f14046c5e4e02aa5fbf65f8524f08ad", size = 15270866, upload-time = "2026-05-06T19:22:34.756Z" }, - { url = "https://files.pythonhosted.org/packages/71/eb/de9ef94958eb2078a6b908ceb247757dc384d3a238d3bd6ed7d81de5eaf8/mypy-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3d26c4321a3b06fc9f04c741e0733af693f82d823f8e64e47b2e63b7f19fa84", size = 16093131, upload-time = "2026-05-06T19:23:56.541Z" }, - { url = "https://files.pythonhosted.org/packages/ad/07/0ab2c1a9d26e90942612724cbd5788f16b7810c5dd39bfcf79286c6c4524/mypy-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bbcbc4d5917ca6ce12de70e051de7f533e3bf92d548b41a38a2232a6fe356525", size = 16330685, upload-time = "2026-05-06T19:21:42.037Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8f/46f85d1371a5be642dad263828118ae1efd536d91d8bd2000c68acff3920/mypy-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:dbc6ba6d40572ae49268531565793a8f07eac7fc65ad76d482c9b4c8765b6043", size = 12752017, upload-time = "2026-05-06T19:22:44.002Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e6/94ca48800cac19eb28a58188a768aaec0d16cac0f373915f073058ab0855/mypy-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:77926029dfcb7e1a3ecb0acb2ddbb24ca36be03f7d623e1759ad5376be8f6c01", size = 10527097, upload-time = "2026-05-06T19:20:58.973Z" }, - { url = "https://files.pythonhosted.org/packages/5c/14/fd0694aa594d6e9f9fd16ce821be2eff295197a273262ef56ddcc1388d68/mypy-2.0.0-py3-none-any.whl", hash = "sha256:8a92b2be3146b4fa1f062af7eb05574cbf3e6eb8e1f14704af1075423144e4e5", size = 2673434, upload-time = "2026-05-06T19:26:32.856Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, ] [[package]] @@ -4442,7 +4446,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "ptyprocess", marker = "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 = [ @@ -5019,10 +5023,10 @@ name = "pyobjc-framework-applicationservices" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "pyobjc-framework-cocoa", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "pyobjc-framework-coretext", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "pyobjc-framework-quartz", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "pyobjc-core", marker = "sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "pyobjc-framework-coretext", marker = "sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/6a/d4e613c8e926a5744fc47a9e9fea08384a510dc4f27d844f7ad7a2d793bd/pyobjc_framework_applicationservices-12.1.tar.gz", hash = "sha256:c06abb74f119bc27aeb41bf1aef8102c0ae1288aec1ac8665ea186a067a8945b", size = 103247, upload-time = "2025-11-14T10:08:52.18Z" } wheels = [ @@ -5038,7 +5042,7 @@ name = "pyobjc-framework-cocoa" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "pyobjc-core", marker = "sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } wheels = [ @@ -5054,9 +5058,9 @@ name = "pyobjc-framework-coretext" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "pyobjc-framework-cocoa", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "pyobjc-framework-quartz", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "pyobjc-core", marker = "sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/da/682c9c92a39f713bd3c56e7375fa8f1b10ad558ecb075258ab6f1cdd4a6d/pyobjc_framework_coretext-12.1.tar.gz", hash = "sha256:e0adb717738fae395dc645c9e8a10bb5f6a4277e73cba8fa2a57f3b518e71da5", size = 90124, upload-time = "2025-11-14T10:14:38.596Z" } wheels = [ @@ -5072,8 +5076,8 @@ name = "pyobjc-framework-quartz" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "pyobjc-framework-cocoa", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "pyobjc-core", marker = "sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } wheels = [ @@ -5536,7 +5540,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.1" +version = "2.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -5544,9 +5548,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/b8/7a707d60fea4c49094e40262cc0e2ca6c768cca21587e34d3f705afec47e/requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a", size = 142436, upload-time = "2026-05-11T19:29:51.717Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60", size = 73021, upload-time = "2026-05-11T19:29:49.923Z" }, ] [[package]] @@ -6129,7 +6133,7 @@ version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess", marker = "os_name != 'nt'" }, - { name = "pywinpty", marker = "os_name == 'nt' and sys_platform != 'linux'" }, + { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') 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" } @@ -6230,15 +6234,19 @@ name = "torch" version = "2.11.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "(python_full_version >= '3.15' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "(python_full_version >= '3.15' and platform_machine != 'arm64' and platform_machine != 's390x' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'emscripten'", "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'emscripten'", - "(python_full_version == '3.14.*' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version == '3.14.*' and platform_machine != 'arm64' and platform_machine != 's390x' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version == '3.13.*' and platform_machine != 'arm64' and platform_machine != 's390x' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", - "(python_full_version < '3.13' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version < '3.13' and platform_machine != 'arm64' and platform_machine != 's390x' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", @@ -6246,13 +6254,13 @@ resolution-markers = [ "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", - "(python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'win32'", "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'win32'", - "(python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'win32')", - "(python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'win32'", "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'win32'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'win32'", - "(python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", ] dependencies = [ @@ -6283,21 +6291,21 @@ version = "2.11.0+cu128" source = { registry = "https://download.pytorch.org/whl/cu128" } resolution-markers = [ "(python_full_version >= '3.15' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'linux')", - "python_full_version >= '3.15' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", - "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'linux'", "(python_full_version == '3.14.*' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux')", - "python_full_version == '3.14.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "(python_full_version == '3.13.*' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')", - "python_full_version == '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "(python_full_version < '3.13' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'linux')", + "(python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'linux')", + "(python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'linux')", + "(python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'linux')", + "python_full_version >= '3.15' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'linux'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'linux'", - "(python_full_version < '3.13' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", - "python_full_version < '3.13' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'linux'", - "(python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'armv7l' and sys_platform == 'linux')", - "(python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", - "(python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", - "(python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'armv7l' and sys_platform == 'linux')", ] dependencies = [ { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, @@ -6366,15 +6374,19 @@ name = "torchvision" version = "0.26.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "(python_full_version >= '3.15' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "(python_full_version >= '3.15' and platform_machine != 'arm64' and platform_machine != 's390x' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'emscripten'", "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'emscripten'", - "(python_full_version == '3.14.*' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version == '3.14.*' and platform_machine != 'arm64' and platform_machine != 's390x' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version == '3.13.*' and platform_machine != 'arm64' and platform_machine != 's390x' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", - "(python_full_version < '3.13' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version < '3.13' and platform_machine != 'arm64' and platform_machine != 's390x' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'emscripten'", @@ -6382,13 +6394,13 @@ resolution-markers = [ "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'emscripten'", "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'emscripten'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'emscripten'", - "(python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version >= '3.15' and platform_machine != 's390x' and sys_platform == 'win32'", "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'win32'", - "(python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'win32')", - "(python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version == '3.14.*' and platform_machine != 's390x' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'win32'", "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'win32'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'win32'", - "(python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'win32')", + "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'win32'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'win32'", ] dependencies = [ @@ -6415,21 +6427,21 @@ version = "0.26.0+cu128" source = { registry = "https://download.pytorch.org/whl/cu128" } resolution-markers = [ "(python_full_version >= '3.15' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'x86_64' and sys_platform == 'linux')", - "python_full_version >= '3.15' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", - "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'linux'", "(python_full_version == '3.14.*' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux')", - "python_full_version == '3.14.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "(python_full_version == '3.13.*' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')", - "python_full_version == '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "(python_full_version < '3.13' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", + "(python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'linux')", + "(python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'linux')", + "(python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'linux')", + "(python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'linux')", + "python_full_version >= '3.15' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.15' and platform_machine == 's390x' and sys_platform == 'linux'", + "python_full_version == '3.14.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version == '3.14.*' and platform_machine == 's390x' and sys_platform == 'linux'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'linux'", - "(python_full_version < '3.13' and platform_machine == 'AMD64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux')", - "python_full_version < '3.13' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 's390x' and platform_machine != 'x86_64' and sys_platform == 'linux'", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'linux'", - "(python_full_version >= '3.15' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version >= '3.15' and platform_machine == 'armv7l' and sys_platform == 'linux')", - "(python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.14.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", - "(python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", - "(python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'armv7l' and sys_platform == 'linux')", ] dependencies = [ { name = "numpy", marker = "sys_platform == 'linux'" }, @@ -6679,7 +6691,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.3.1" +version = "21.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -6687,9 +6699,9 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e1/665267cea4767debd19f584667a9197c2098b5e7f67a502da9f3a086ab37/virtualenv-21.3.2.tar.gz", hash = "sha256:3ecda97894a6fc1c53106356f488690e5c86278c1f693f3fc0805ac85a513686", size = 7613810, upload-time = "2026-05-12T14:44:18.01Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" }, + { url = "https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl", hash = "sha256:c58ea748fa50bb2a4367da5ba3d30b02458ed40b4ea888faad94021f3309f764", size = 7594558, upload-time = "2026-05-12T14:44:15.193Z" }, ] [[package]] From fe96b28c7457629588dd113df11bb853981587f4 Mon Sep 17 00:00:00 2001 From: Jash Shah Date: Wed, 13 May 2026 00:45:27 -0700 Subject: [PATCH 13/18] Fix policy.path not working in YAML config files (#3145) * fix(config): support policy.path in YAML config files policy.path was only handled via CLI args (filtered from sys.argv before draccus, then retrieved in validate()). When specified in YAML, draccus would crash because 'path' is not a valid field on PreTrainedConfig. Extract path fields from the YAML/JSON config before draccus processes it, store them in a module-level dict, and fall back to it in get_path_arg() when the CLI doesn't have the path. Fixes #2957 * fix(parser): preserve YAML policy overrides when loading from pretrained When policy.path is set in YAML, validate() was calling from_pretrained with only CLI overrides, discarding any YAML policy fields (e.g. lr, batch_size) that draccus had already parsed. Fix by capturing the remaining YAML fields as CLI-style args in _config_yaml_overrides and merging them into the overrides passed to from_pretrained in train.py, eval.py, and lerobot_record.py (CLI args still take precedence). Also fix the NamedTemporaryFile SIM115 ruff warning and add types-PyYAML to the mypy pre-commit hook. * fix(parser): serialize bool/None values correctly in YAML policy overrides Bool values from YAML configs (e.g. push_to_hub: true) were passed as Python "True"/"False" strings instead of lowercase "true"/"false" that draccus expects. Also skip None values to avoid passing "None" strings. * revert: remove types-PyYAML from .pre-commit-config.yaml * chore: fix quality check caused by untyped YAML import Co-authored-by: masato-ka Signed-off-by: Khalil Meftah --------- Signed-off-by: Khalil Meftah Co-authored-by: Khalil Meftah Co-authored-by: masato-ka --- src/lerobot/configs/eval.py | 7 +- src/lerobot/configs/parser.py | 84 ++++++++++++- src/lerobot/configs/train.py | 7 +- tests/test_yaml_policy_path.py | 218 +++++++++++++++++++++++++++++++++ 4 files changed, 311 insertions(+), 5 deletions(-) create mode 100644 tests/test_yaml_policy_path.py diff --git a/src/lerobot/configs/eval.py b/src/lerobot/configs/eval.py index d1cebd27f..f2a1d3065 100644 --- a/src/lerobot/configs/eval.py +++ b/src/lerobot/configs/eval.py @@ -46,8 +46,11 @@ class EvalPipelineConfig: # HACK: We parse again the cli args here to get the pretrained path if there was one. policy_path = parser.get_path_arg("policy") if policy_path: - cli_overrides = parser.get_cli_overrides("policy") - self.policy = PreTrainedConfig.from_pretrained(policy_path, cli_overrides=cli_overrides) + yaml_overrides = parser.get_yaml_overrides("policy") + cli_overrides = parser.get_cli_overrides("policy") or [] + self.policy = PreTrainedConfig.from_pretrained( + policy_path, cli_overrides=yaml_overrides + cli_overrides + ) self.policy.pretrained_path = Path(policy_path) else: diff --git a/src/lerobot/configs/parser.py b/src/lerobot/configs/parser.py index 57ebaf8fa..d55fa44aa 100644 --- a/src/lerobot/configs/parser.py +++ b/src/lerobot/configs/parser.py @@ -13,8 +13,10 @@ # limitations under the License. import importlib import inspect +import json import pkgutil import sys +import tempfile from argparse import ArgumentError from collections.abc import Callable, Iterable, Sequence from functools import wraps @@ -24,6 +26,7 @@ from types import ModuleType from typing import Any, TypeVar, cast import draccus +import yaml # type: ignore[import-untyped] from lerobot.utils.utils import has_method @@ -32,6 +35,29 @@ F = TypeVar("F", bound=Callable[..., object]) PATH_KEY = "path" PLUGIN_DISCOVERY_SUFFIX = "discover_packages_path" +# Storage for path args extracted from YAML/JSON config files, so that +# get_path_arg() can find them even when they weren't passed via CLI. +_config_path_args: dict[str, str] = {} + +# Storage for non-path YAML overrides so validate() can pass them to from_pretrained. +_config_yaml_overrides: dict[str, list[str]] = {} + + +def _flatten_to_cli_args(d: dict, prefix: str = "") -> list[str]: + """Recursively flatten a nested dict to CLI-style args (e.g. {"lr": 1e-4} -> ["--lr=0.0001"]).""" + args = [] + for key, value in d.items(): + if key in (PATH_KEY, draccus.CHOICE_TYPE_KEY): + continue + full_key = f"{prefix}.{key}" if prefix else key + if isinstance(value, bool): + value = str(value).lower() + if isinstance(value, dict): + args.extend(_flatten_to_cli_args(value, full_key)) + elif value is not None and not isinstance(value, list): + args.append(f"--{full_key}={value}") + return args + def get_cli_overrides(field_name: str, args: Sequence[str] | None = None) -> list[str] | None: """Parses arguments from cli at a given nested attribute level. @@ -145,7 +171,14 @@ def load_plugin(plugin_path: str) -> None: def get_path_arg(field_name: str, args: Sequence[str] | None = None) -> str | None: - return parse_arg(f"{field_name}.{PATH_KEY}", args) + result = parse_arg(f"{field_name}.{PATH_KEY}", args) + if result is None: + result = _config_path_args.get(field_name) + return result + + +def get_yaml_overrides(field_name: str) -> list[str]: + return _config_yaml_overrides.get(field_name, []) def get_type_arg(field_name: str, args: Sequence[str] | None = None) -> str | None: @@ -192,6 +225,52 @@ def filter_path_args(fields_to_filter: str | list[str], args: Sequence[str] | No return filtered_args +def extract_path_fields_from_config(config_path: str, path_fields: list[str]) -> str: + """Extract `path` fields from a YAML/JSON config before draccus processes it. + + When a user specifies e.g. ``policy.path: lerobot/smolvla_base`` in a YAML config, + draccus will fail because ``path`` is not a valid field on policy config classes. + This function extracts those path values, stores them in ``_config_path_args`` for + later retrieval by ``get_path_arg()``, and returns a cleaned temp config file path. + """ + config_file = Path(config_path) + suffix = config_file.suffix.lower() + + if suffix in (".yaml", ".yml"): + with open(config_file) as f: + config_data = yaml.safe_load(f) + elif suffix == ".json": + with open(config_file) as f: + config_data = json.load(f) + else: + return config_path + + if not isinstance(config_data, dict): + return config_path + + modified = False + for field in path_fields: + if field in config_data and isinstance(config_data[field], dict) and PATH_KEY in config_data[field]: + _config_path_args[field] = str(config_data[field].pop(PATH_KEY)) + remaining = config_data[field] + if remaining: + _config_yaml_overrides[field] = _flatten_to_cli_args(remaining) + else: + del config_data[field] + modified = True + + if not modified: + return config_path + + # Write cleaned config to a temp file + with tempfile.NamedTemporaryFile(mode="w", suffix=suffix, delete=False) as tmp: + if suffix in (".yaml", ".yml"): + yaml.dump(config_data, tmp, default_flow_style=False) + else: + json.dump(config_data, tmp, indent=2) + return tmp.name + + def wrap(config_path: Path | None = None) -> Callable[[F], F]: """ HACK: Similar to draccus.wrap but does three additional things: @@ -225,6 +304,9 @@ def wrap(config_path: Path | None = None) -> Callable[[F], F]: if has_method(argtype, "__get_path_fields__"): path_fields = argtype.__get_path_fields__() cli_args = filter_path_args(path_fields, cli_args) + # Also extract path fields from the YAML/JSON config file + if config_path_cli: + config_path_cli = extract_path_fields_from_config(config_path_cli, path_fields) if has_method(argtype, "from_pretrained") and config_path_cli: cli_args = filter_arg("config_path", cli_args) cfg = argtype.from_pretrained(config_path_cli, cli_args=cli_args) diff --git a/src/lerobot/configs/train.py b/src/lerobot/configs/train.py index 388de9437..c5b4ff5f5 100644 --- a/src/lerobot/configs/train.py +++ b/src/lerobot/configs/train.py @@ -144,8 +144,11 @@ class TrainPipelineConfig(HubMixin): ) self.reward_model.pretrained_path = str(Path(reward_model_path)) elif policy_path: - cli_overrides = parser.get_cli_overrides("policy") - self.policy = PreTrainedConfig.from_pretrained(policy_path, cli_overrides=cli_overrides) + yaml_overrides = parser.get_yaml_overrides("policy") + cli_overrides = parser.get_cli_overrides("policy") or [] + self.policy = PreTrainedConfig.from_pretrained( + policy_path, cli_overrides=yaml_overrides + cli_overrides + ) self.policy.pretrained_path = Path(policy_path) elif self.resume: config_path = parser.parse_arg("config_path") diff --git a/tests/test_yaml_policy_path.py b/tests/test_yaml_policy_path.py new file mode 100644 index 000000000..710a71c9a --- /dev/null +++ b/tests/test_yaml_policy_path.py @@ -0,0 +1,218 @@ +"""Tests for policy.path support in YAML config files (issue #2957).""" + +import json +import tempfile + +import yaml + +from lerobot.configs.parser import ( + _config_path_args, + _config_yaml_overrides, + _flatten_to_cli_args, + extract_path_fields_from_config, + get_path_arg, + get_yaml_overrides, +) + + +def test_extract_path_fields_from_yaml(): + """Test that policy.path is extracted from a YAML config and removed.""" + config = { + "dataset": {"repo_id": "lerobot/pusht"}, + "policy": {"type": "smolvla", "path": "lerobot/smolvla_base", "push_to_hub": False}, + } + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(config, f) + config_path = f.name + + _config_path_args.clear() + cleaned_path = extract_path_fields_from_config(config_path, ["policy"]) + + # Path should be extracted and stored + assert _config_path_args["policy"] == "lerobot/smolvla_base" + + # Cleaned config should not have the path field + with open(cleaned_path) as f: + cleaned = yaml.safe_load(f) + assert "path" not in cleaned["policy"] + assert cleaned["policy"]["type"] == "smolvla" + assert cleaned["policy"]["push_to_hub"] is False + + # Original dataset should be untouched + assert cleaned["dataset"]["repo_id"] == "lerobot/pusht" + + _config_path_args.clear() + + +def test_extract_path_fields_from_json(): + """Test that policy.path is extracted from a JSON config.""" + config = { + "policy": {"type": "act", "path": "some/local/path"}, + } + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + json.dump(config, f) + config_path = f.name + + _config_path_args.clear() + cleaned_path = extract_path_fields_from_config(config_path, ["policy"]) + + assert _config_path_args["policy"] == "some/local/path" + + with open(cleaned_path) as f: + cleaned = json.load(f) + assert "path" not in cleaned["policy"] + + _config_path_args.clear() + + +def test_extract_no_path_returns_original(): + """Test that configs without path fields are returned unchanged.""" + config = { + "dataset": {"repo_id": "lerobot/pusht"}, + "policy": {"type": "smolvla"}, + } + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(config, f) + config_path = f.name + + _config_path_args.clear() + result = extract_path_fields_from_config(config_path, ["policy"]) + + assert result == config_path + assert "policy" not in _config_path_args + + _config_path_args.clear() + + +def test_extract_removes_empty_field(): + """Test that the field dict is removed entirely if path was the only key.""" + config = { + "dataset": {"repo_id": "lerobot/pusht"}, + "policy": {"path": "lerobot/smolvla_base"}, + } + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(config, f) + config_path = f.name + + _config_path_args.clear() + cleaned_path = extract_path_fields_from_config(config_path, ["policy"]) + + assert _config_path_args["policy"] == "lerobot/smolvla_base" + + with open(cleaned_path) as f: + cleaned = yaml.safe_load(f) + assert "policy" not in cleaned + + _config_path_args.clear() + + +def test_get_path_arg_fallback(): + """Test that get_path_arg falls back to _config_path_args when CLI has no path.""" + _config_path_args.clear() + _config_path_args["policy"] = "lerobot/smolvla_base" + + # No CLI args with --policy.path + result = get_path_arg("policy", args=[]) + assert result == "lerobot/smolvla_base" + + _config_path_args.clear() + + +def test_get_path_arg_cli_takes_precedence(): + """Test that CLI --policy.path takes precedence over YAML config path.""" + _config_path_args.clear() + _config_path_args["policy"] = "yaml/path" + + result = get_path_arg("policy", args=["--policy.path=cli/path"]) + assert result == "cli/path" + + _config_path_args.clear() + + +def test_yaml_overrides_captured(): + """Test that non-path policy fields are captured as CLI-style overrides.""" + config = { + "policy": {"path": "lerobot/smolvla_base", "lr": 1e-4, "batch_size": 32}, + } + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(config, f) + config_path = f.name + + _config_path_args.clear() + _config_yaml_overrides.clear() + extract_path_fields_from_config(config_path, ["policy"]) + + overrides = get_yaml_overrides("policy") + assert "--lr=0.0001" in overrides or any("lr=" in o for o in overrides) + assert any("batch_size=32" in o for o in overrides) + + _config_path_args.clear() + _config_yaml_overrides.clear() + + +def test_yaml_overrides_excludes_type_and_path(): + """Test that type and path fields are not included in YAML overrides.""" + config = { + "policy": {"path": "lerobot/smolvla_base", "type": "smolvla", "lr": 5e-5}, + } + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(config, f) + config_path = f.name + + _config_path_args.clear() + _config_yaml_overrides.clear() + extract_path_fields_from_config(config_path, ["policy"]) + + overrides = get_yaml_overrides("policy") + assert not any("path=" in o for o in overrides) + assert not any("type=" in o for o in overrides) + assert any("lr=" in o for o in overrides) + + _config_path_args.clear() + _config_yaml_overrides.clear() + + +def test_get_yaml_overrides_empty_when_path_only(): + """Test that get_yaml_overrides returns [] when policy had only a path field.""" + config = { + "policy": {"path": "lerobot/smolvla_base"}, + } + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(config, f) + config_path = f.name + + _config_path_args.clear() + _config_yaml_overrides.clear() + extract_path_fields_from_config(config_path, ["policy"]) + + assert get_yaml_overrides("policy") == [] + + _config_path_args.clear() + _config_yaml_overrides.clear() + + +def test_flatten_bool_values(): + """Test that boolean values are serialized as lowercase strings for draccus.""" + d = {"push_to_hub": True, "use_rabc": False, "lr": 0.001, "name": "test"} + args = _flatten_to_cli_args(d) + assert "--push_to_hub=true" in args + assert "--use_rabc=false" in args + assert "--lr=0.001" in args + assert "--name=test" in args + + +def test_flatten_none_values_skipped(): + """Test that None values are not included in flattened args.""" + d = {"lr": 0.001, "path_override": None, "name": "test"} + args = _flatten_to_cli_args(d) + assert any("lr=" in a for a in args) + assert any("name=" in a for a in args) + assert not any("path_override" in a for a in args) + + +def test_flatten_nested_with_bools(): + """Test that bools in nested dicts are handled correctly.""" + d = {"optimizer": {"use_warmup": True, "lr": 0.01}} + args = _flatten_to_cli_args(d) + assert "--optimizer.use_warmup=true" in args + assert "--optimizer.lr=0.01" in args From 9db9c35cb473afe3d9bf7b54fa5fd44576974f75 Mon Sep 17 00:00:00 2001 From: Cheng Yin <34277382+wadeKeith@users.noreply.github.com> Date: Wed, 13 May 2026 17:09:19 +0800 Subject: [PATCH 14/18] fix(config): add lora_alpha to PeftConfig (#3573) * fix(config): add lora_alpha to PeftConfig PeftConfig was missing the lora_alpha field, causing the PEFT library to default to alpha=8 regardless of the LoRA rank, which dampens the adaptation signal for high-rank adapters (e.g., r=128). This adds lora_alpha: int | None = None to PeftConfig, allowing users to specify --peft.lora_alpha on the CLI. Closes #3551 * fix(docs): add lora_alpha to peft training example + clarify scaling formula - Add --peft.lora_alpha=64 to docs/source/peft_training.mdx example to prevent new users from hitting the alpha=8 default dampening bug - Clarify lora_alpha comment in default.py with scaling = lora_alpha / r * docs: mention both --peft.r and --peft.lora_alpha in LoRA description --------- Co-authored-by: Cheng Yin --- docs/source/peft_training.mdx | 6 ++++-- src/lerobot/configs/default.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/source/peft_training.mdx b/docs/source/peft_training.mdx index dd0b10075..d44a3f081 100644 --- a/docs/source/peft_training.mdx +++ b/docs/source/peft_training.mdx @@ -28,13 +28,15 @@ lerobot-train \ --steps=100000 \ --batch_size=32 \ --peft.method_type=LORA \ - --peft.r=64 + --peft.r=64 \ + --peft.lora_alpha=64 ``` Note the `--peft.method_type` parameter that let's you select which PEFT method to use. Here we use [LoRA](https://huggingface.co/docs/peft/main/en/package_reference/lora) (Low-Rank Adapter) which is probably the most popular fine-tuning method to date. Low-rank adaption means that we only fine-tune a matrix with comparably low rank -instead of the full weight matrix. This rank can be specified using the `--peft.r` parameter. The higher the rank +instead of the full weight matrix. This rank can be specified using the `--peft.r` parameter, and the LoRA scaling factor with +`--peft.lora_alpha` (where `scaling = lora_alpha / r`). The higher the rank the closer you get to full fine-tuning There are more complex methods that have more parameters. These are not yet supported, feel free to raise an issue diff --git a/src/lerobot/configs/default.py b/src/lerobot/configs/default.py index be906edbd..b1eebba94 100644 --- a/src/lerobot/configs/default.py +++ b/src/lerobot/configs/default.py @@ -117,3 +117,9 @@ class PeftConfig: # the rank used for the adapter. In general a higher rank means more trainable parameters and closer to full # fine-tuning. r: int = 16 + + # Alpha parameter for LoRA scaling (scaling = lora_alpha / r). + # In general, a higher alpha means stronger adaptation signal. + # If None, the PEFT library defaults to alpha=8, which may dampen high-rank adapters. + # Common values are r (alpha == rank) or 2*r. + lora_alpha: int | None = None From ca9028ad64fcb1d263f00cf81a81951b29e7d213 Mon Sep 17 00:00:00 2001 From: Nikodem Bartnik <39432165+NikodemBartnik@users.noreply.github.com> Date: Thu, 14 May 2026 12:32:39 +0200 Subject: [PATCH 15/18] docs(quickstart): adding rollout (#3598) * fix whoami command * include lerobot-rollout in inference section --- examples/notebooks/quickstart.ipynb | 31 +++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/examples/notebooks/quickstart.ipynb b/examples/notebooks/quickstart.ipynb index 647b79506..16034a687 100644 --- a/examples/notebooks/quickstart.ipynb +++ b/examples/notebooks/quickstart.ipynb @@ -80,7 +80,7 @@ "}\n", "\n", "# Dataset\n", - "HF_USER = \"your_hf_username\" # `huggingface-cli whoami` to find your username\n", + "HF_USER = \"your_hf_username\" # `hf auth whoami` to find your username\n", "DATASET_NAME = \"my_so101_dataset\"\n", "TASK_DESCRIPTION = \"pick and place the block\"\n", "NUM_EPISODES = 10\n", @@ -291,7 +291,34 @@ "\n", "Uses `POLICY_PATH` from the Configuration cell (defaults to the Hub repo ID). You can also put there the `LAST_CHECKPOINT_PATH`.\n", "\n", - "See the [inference docs](https://huggingface.co/docs/lerobot/il_robots#run-inference-and-evaluate-your-policy) for details." + "See the [inference docs](https://huggingface.co/docs/lerobot/il_robots#run-inference-and-evaluate-your-policy) for details.\n", + "\n", + "Recently ```lerobot-rollout``` was introduced, you can [read more about it here](https://huggingface.co/docs/lerobot/main/en/il_robots?eval=Base+mode+%28no+recording%29#run-inference-and-evaluate-your-policy)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print_cmd(\n", + " \"lerobot-rollout\",\n", + " \"--strategy.type=base\",\n", + " f\"--policy.path={POLICY_PATH}\",\n", + " f\"--robot.type={ROBOT_TYPE}\",\n", + " f\"--robot.port={ROBOT_PORT}\",\n", + " CAMERAS_FLAG,\n", + " f'--task=\"{TASK_DESCRIPTION}\"',\n", + " \"--duration=60\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "if you are using the V0.5.1 release you should use ```lerobot-record``` instead of rollout" ] }, { From 0a4a7c40ad57af74883e910ae92f1c6699913617 Mon Sep 17 00:00:00 2001 From: Nikodem Bartnik <39432165+NikodemBartnik@users.noreply.github.com> Date: Thu, 14 May 2026 15:11:35 +0200 Subject: [PATCH 16/18] docs(cheat sheet): create cheat sheet (#3602) * add comprehensive CLI cheat sheet for quick reference --- docs/source/_toctree.yml | 2 + docs/source/cheat-sheet.mdx | 139 ++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 docs/source/cheat-sheet.mdx diff --git a/docs/source/_toctree.yml b/docs/source/_toctree.yml index 87fcacf42..de4eeaa28 100644 --- a/docs/source/_toctree.yml +++ b/docs/source/_toctree.yml @@ -3,6 +3,8 @@ title: LeRobot - local: installation title: Installation + - local: cheat-sheet + title: Cheat sheet title: Get started - sections: - local: il_robots diff --git a/docs/source/cheat-sheet.mdx b/docs/source/cheat-sheet.mdx new file mode 100644 index 000000000..a6afa14c2 --- /dev/null +++ b/docs/source/cheat-sheet.mdx @@ -0,0 +1,139 @@ +# Cheat sheet + +All of the LeRobot commands in one place. If you forgot how to use a specific command or want to learn about a new one you can do it here. + +> [!WARNING] +> For all of the commands listed below remember to change the ports/names/ids to your own values! + +> [!TIP] +> Another great way to look at all the commands and get them configured for your specific setup is to use this [Jupyter Notebook](https://github.com/huggingface/lerobot/blob/main/examples/notebooks/quickstart.ipynb). + +### Setup and installation + +For installation please look at [LeRobot Installation](https://huggingface.co/docs/lerobot/main/en/installation). + +### Useful tools + +###### Find port + +Use this to identify which serial ports your robots are connected to. Follow the instructions in your terminal: you will be asked to unplug the USB cable and press Enter. The script will then detect and print the correct serial port for that robot. + +```bash +lerobot-find-port +``` + +###### Find cameras + +Quickly find camera indices and verify their output. This command prints camera information to the terminal and saves test frames from each detected camera to `lerobot/outputs/captured_images` + +```bash +lerobot-find-cameras +``` + +### Calibration + +In most cases you will need to perform calibration just once for each robot and teleoperation device. Before performing the calibration make sure that all the joints are roughly in the middle position. + +```bash +lerobot-calibrate \ + --robot.type=so101_follower \ + --robot.port=/dev/ttyACM0 \ + --robot.id=my_follower_arm +``` + +Make sure that you use the same IDs used during calibration later for the other scripts. That's how LeRobot finds the calibration files. + +### Teleoperation + +Teleoperating with two cameras and displaying the data with Rerun. + +```bash +lerobot-teleoperate \ + --robot.type=so101_follower \ + --robot.port=/dev/ttyACM0 \ + --robot.id=my_follower_arm \ + --robot.cameras="{ top: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30} }" \ + --teleop.type=so101_leader \ + --teleop.port=/dev/ttyACM1 \ + --teleop.id=my_leader_arm \ + --display_data=true +``` + +### Recording a dataset + +The dataset is automatically uploaded to the server and saved under repo_id, make sure you are logged in to your HF account with CLI: +`hf auth login` + +You can get the token from: [https://huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) + +```bash +lerobot-record \ + --robot.type=so101_follower \ + --robot.port=/dev/ttyACM0 \ + --robot.id=my_follower_arm \ + --robot.cameras="{ top: {type: opencv, index_or_path: 1, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30} }" \ + --teleop.type=so101_leader \ + --teleop.port=/dev/ttyACM1 \ + --teleop.id=my_leader_arm \ + --dataset.repo_id=${HF_USER}/so101_dataset_test \ + --dataset.num_episodes=30 \ + --dataset.single_task="put the red brick in a bowl" \ + --dataset.streaming_encoding=true \ + --display_data=true +``` + +While collecting the dataset you can control the process with your keyboard: +Control the data recording flow using keyboard shortcuts: + +- Press **Right Arrow (`→`)**: Save episode and move to the next. +- Press **Left Arrow (`←`)**: Delete current episode and retry. +- Press **Escape (`ESC`)**: Stop, encode videos, and upload. + +### Training + +Depending on your hardware training the policy might take a few hours. That's how you train simple `ACT` policy: + +```bash +lerobot-train \ + --dataset.repo_id=${HF_USER}/so101_dataset_test \ + --policy.type=act \ + --output_dir=outputs/train/act_so101_test \ + --job_name=act_so101_test \ + --policy.device=cuda \ + --wandb.enable=true \ + --policy.repo_id=${HF_USER}/policy_test \ + --steps=20000 +``` + +- Policy Types: `act`, `diffusion`, `smolvla`, `pi05` +- Devices: `cuda` (NVIDIA), `mps` (Apple Silicon), `cpu` + +If you want to fine-tune a specific model you can provide the path to the model. In this case path is enough and type can be skipped. + +```bash +lerobot-train \ + --dataset.repo_id=${HF_USER}/so101_dataset_test \ + --policy.path=username/the_policy_to_finetune \ + --policy.device=cuda \ + --policy.repo_id=${HF_USER}/policy_test \ + --output_dir=outputs/train/act_so101_test \ + --steps=20000 +``` + +### Inference + +Inference means running the trained policy/model on a robot. For that we use `lerobot-rollout`. You will need to provide a path to your policy. It can be a local path or a path to Hugging Face for example "lerobot/folding_latest". Your cameras configuration needs to match what was used when collecting the dataset. Duration is in seconds if unspecified, it will run forever. + +> [!TIP] +> If you are using the previous release V0.5.1 instead of `lerobot-rollout` you need to use `lerobot-record`. More information [here](https://huggingface.co/docs/lerobot/v0.5.1/en/il_robots#run-inference-and-evaluate-your-policy). + +```bash +lerobot-rollout \ + --strategy.type=base \ + --policy.path=${HF_USER}/my_policy \ + --robot.type=so101_follower \ + --robot.port=/dev/ttyACM1 \ + --robot.cameras="{ up: {type: opencv, index_or_path: /dev/video1, width: 640, height: 480, fps: 30}, side: {type: opencv, index_or_path: /dev/video5, width: 640, height: 480, fps: 30}}" \ + --task="Put lego brick into the transparent box" \ + --duration=60 +``` From bd9619dfc3fadd3647408537d89e82f83f770851 Mon Sep 17 00:00:00 2001 From: Caroline Pascal Date: Thu, 14 May 2026 23:46:42 +0200 Subject: [PATCH 17/18] feat(encoding parameters): adding support for user provided video encoding parameters (#3455) * chore(video backend): renaming codec into video_backend in get_safe_default_video_backend() * feat(pyav utils): adding suport for PyAV encoding parameters validation * feat(VideoEncoderConfig): creating a VideoEncoderConfig to encapsulate encoding parameters * feat(VideoEncoderConfig): propagating the VideoEncoderConfig in the codebase * chore(docs): updating the docs * feat(metadata): adding encoding parameters in dataset metadata * fix(concatenation compatibility): adding compatibility check when concatenating video files * feat(VideoEncoderConfig init): making VideoEncoderConfig more robust and adaptable to multiple backends * feat(pyav checks): making pyav parameters checks more robust * chore(duplicate): removing duplicate get_codec_options definition * test(existing): adapting existing tests * test(new): adding new tests for encoding related features * chore(format): fixing formatting issues * chore(PyAV): cleaning up PyAV utils and encoding parameters checks to stick to the minimun required tooling. * chore(format): formatting code * chore(doctrings): updating docstrings * fix(camera_encoder_config): Removing camera_encoder_config from LeRobotDataset, as it's only required in LeRobotDatasetWriter. * feat(default values): applying a consistent naming convention for default RGB cameras video encoder parameters * fix(rollout): propagating VideoEncoderConfig to the latest recording modes * chore(format): formatting code, fixing error messages and variable names * fix(arguments order): reverting changes in arguments order in StreamingVideoEncoder * chore(relative imports): switching to relative local imports within lerobot.datasets * test(artifacts): cleaning up artifacts for the video encoding tests * chore(docs): updating docs * chore(fromat): formatting code * fix(imports): refactoring the file architecture to avoid circular imports. VideoEncoderConfig is now defined in lerobot.configs and lazily imports av at runtime. * fix(typos): fixing typos and small mistakes * test(factories): updating factories * feat(aggregate): updating dataset aggregation procedure. Encoding tuning paramters (crf, g,...) are ignored for validation and changed to None in the aggregated dataset if incompatible. * docs(typos): fixing typos * fix(deletion): reverting unwanted deletion * fix(typos): fixing multiple typos * feat(codec options): passing codec options to lerobot_edit_dataset episode deletion tool * typo(typo): typo * fix(typos): fixing remaining typos * chore(rename): renaming camera_encoder_config to camera_encoder * docs(clean): cleaning and formating docs * docs(dataset): addind details about datasets * chore(format): formatting code * docs(warning): adding warning regarding encoding parameters modification * fix(re-encoding): removing inconsistent re-encoding option in lerobot_edit_dataset * typos(typos): typos * chore(format): resolving prettier issues * fix(h264_nvenc): fixing crf handling for h264_nvenc * docs(clean): removing too technical parts of the docs * fix(imports): fixing imports at the __init__ level * fix(imports): fixing not very pretty imports in video config file --- docs/source/_toctree.yml | 2 + docs/source/act.mdx | 2 +- docs/source/earthrover_mini_plus.mdx | 2 +- docs/source/groot.mdx | 2 +- docs/source/hope_jr.mdx | 4 +- docs/source/il_robots.mdx | 2 +- docs/source/lerobot-dataset-v3.mdx | 2 +- docs/source/reachy2.mdx | 4 +- docs/source/smolvla.mdx | 2 +- docs/source/streaming_video_encoding.mdx | 44 +- docs/source/using_dataset_tools.mdx | 14 +- docs/source/video_encoding_parameters.mdx | 117 ++++ src/lerobot/configs/__init__.py | 12 + src/lerobot/configs/dataset.py | 11 +- src/lerobot/configs/default.py | 4 +- src/lerobot/configs/eval.py | 2 +- src/lerobot/configs/rewards.py | 3 +- src/lerobot/configs/train.py | 2 +- src/lerobot/configs/video.py | 235 +++++++ src/lerobot/datasets/__init__.py | 3 + src/lerobot/datasets/aggregate.py | 56 +- src/lerobot/datasets/dataset_metadata.py | 20 +- src/lerobot/datasets/dataset_tools.py | 94 ++- src/lerobot/datasets/dataset_writer.py | 34 +- src/lerobot/datasets/feature_utils.py | 36 ++ src/lerobot/datasets/lerobot_dataset.py | 79 ++- src/lerobot/datasets/pyav_utils.py | 174 +++++ src/lerobot/datasets/video_utils.py | 228 +++---- src/lerobot/policies/eo1/modeling_eo1.py | 5 +- src/lerobot/policies/eo1/processor_eo1.py | 3 +- .../rewards/classifier/modeling_classifier.py | 5 +- .../classifier/processor_classifier.py | 3 +- src/lerobot/rewards/factory.py | 7 +- .../rewards/sarm/compute_rabc_weights.py | 7 +- src/lerobot/rewards/sarm/modeling_sarm.py | 9 +- src/lerobot/rewards/sarm/processor_sarm.py | 9 +- src/lerobot/rollout/context.py | 4 +- src/lerobot/scripts/lerobot_edit_dataset.py | 16 +- src/lerobot/scripts/lerobot_record.py | 31 +- src/lerobot/scripts/lerobot_rollout.py | 12 + src/lerobot/transport/utils.py | 3 +- src/lerobot/utils/import_utils.py | 5 +- tests/artifacts/encoded_videos/clip_32x48.mp4 | 3 + .../artifacts/encoded_videos/clip_4frames.mp4 | 3 + .../artifacts/encoded_videos/clip_5frames.mp4 | 3 + .../artifacts/encoded_videos/clip_6frames.mp4 | 3 + tests/artifacts/encoded_videos/clip_h264.mp4 | 3 + tests/datasets/test_aggregate.py | 76 ++- tests/datasets/test_dataset_reader.py | 6 +- tests/datasets/test_dataset_tools.py | 13 +- tests/datasets/test_dataset_writer.py | 24 +- tests/datasets/test_datasets.py | 14 +- .../datasets/test_streaming_video_encoder.py | 221 +++---- tests/datasets/test_video_encoding.py | 595 ++++++++++++++++++ tests/fixtures/constants.py | 14 +- tests/fixtures/dataset_factories.py | 5 +- 56 files changed, 1765 insertions(+), 527 deletions(-) create mode 100644 docs/source/video_encoding_parameters.mdx create mode 100644 src/lerobot/configs/video.py create mode 100644 src/lerobot/datasets/pyav_utils.py create mode 100644 tests/artifacts/encoded_videos/clip_32x48.mp4 create mode 100644 tests/artifacts/encoded_videos/clip_4frames.mp4 create mode 100644 tests/artifacts/encoded_videos/clip_5frames.mp4 create mode 100644 tests/artifacts/encoded_videos/clip_6frames.mp4 create mode 100644 tests/artifacts/encoded_videos/clip_h264.mp4 create mode 100644 tests/datasets/test_video_encoding.py diff --git a/docs/source/_toctree.yml b/docs/source/_toctree.yml index de4eeaa28..f1dfe9aae 100644 --- a/docs/source/_toctree.yml +++ b/docs/source/_toctree.yml @@ -41,6 +41,8 @@ title: Using the Dataset Tools - local: dataset_subtask title: Using Subtasks in the Dataset + - local: video_encoding_parameters + title: Video encoding parameters - local: streaming_video_encoding title: Streaming Video Encoding title: "Datasets" diff --git a/docs/source/act.mdx b/docs/source/act.mdx index 453bcbba8..8e91edcf9 100644 --- a/docs/source/act.mdx +++ b/docs/source/act.mdx @@ -90,6 +90,6 @@ lerobot-record \ --dataset.single_task="Your task description" \ --dataset.streaming_encoding=true \ --dataset.encoder_threads=2 \ - # --dataset.vcodec=auto \ + # --dataset.camera_encoder.vcodec=auto \ --policy.path=${HF_USER}/act_policy ``` diff --git a/docs/source/earthrover_mini_plus.mdx b/docs/source/earthrover_mini_plus.mdx index a87bd325b..508c0e3a9 100644 --- a/docs/source/earthrover_mini_plus.mdx +++ b/docs/source/earthrover_mini_plus.mdx @@ -194,7 +194,7 @@ lerobot-record \ --dataset.single_task="Navigate around obstacles" \ --dataset.streaming_encoding=true \ --dataset.encoder_threads=2 \ - # --dataset.vcodec=auto \ + # --dataset.camera_encoder.vcodec=auto \ --display_data=true ``` diff --git a/docs/source/groot.mdx b/docs/source/groot.mdx index 2f53a4d0b..d69d10a57 100644 --- a/docs/source/groot.mdx +++ b/docs/source/groot.mdx @@ -123,7 +123,7 @@ lerobot-record \ --dataset.single_task="Grab and handover the red cube to the other arm" \ --dataset.streaming_encoding=true \ --dataset.encoder_threads=2 \ - # --dataset.vcodec=auto \ + # --dataset.camera_encoder.vcodec=auto \ --policy.path=/groot-bimanual \ # your trained model --dataset.episode_time_s=30 \ --dataset.reset_time_s=10 diff --git a/docs/source/hope_jr.mdx b/docs/source/hope_jr.mdx index 8826d9758..1f3b08fd7 100644 --- a/docs/source/hope_jr.mdx +++ b/docs/source/hope_jr.mdx @@ -232,7 +232,7 @@ lerobot-record \ --dataset.private=true \ --dataset.streaming_encoding=true \ --dataset.encoder_threads=2 \ - # --dataset.vcodec=auto \ + # --dataset.camera_encoder.vcodec=auto \ --display_data=true ``` @@ -278,6 +278,6 @@ lerobot-record \ --dataset.num_episodes=10 \ --dataset.streaming_encoding=true \ --dataset.encoder_threads=2 \ - # --dataset.vcodec=auto \ + # --dataset.camera_encoder.vcodec=auto \ --policy.path=outputs/train/hopejr_hand/checkpoints/last/pretrained_model ``` diff --git a/docs/source/il_robots.mdx b/docs/source/il_robots.mdx index ff0a6229e..07789225a 100644 --- a/docs/source/il_robots.mdx +++ b/docs/source/il_robots.mdx @@ -193,7 +193,7 @@ lerobot-record \ --dataset.num_episodes=5 \ --dataset.single_task="Grab the black cube" \ --dataset.streaming_encoding=true \ - # --dataset.vcodec=auto \ + # --dataset.camera_encoder.vcodec=auto \ --dataset.encoder_threads=2 ``` diff --git a/docs/source/lerobot-dataset-v3.mdx b/docs/source/lerobot-dataset-v3.mdx index 8ab4a5d40..6f3e6d948 100644 --- a/docs/source/lerobot-dataset-v3.mdx +++ b/docs/source/lerobot-dataset-v3.mdx @@ -43,7 +43,7 @@ lerobot-record \ --dataset.num_episodes=5 \ --dataset.single_task="Grab the black cube" \ --dataset.streaming_encoding=true \ - # --dataset.vcodec=auto \ + # --dataset.camera_encoder.vcodec=auto \ --dataset.encoder_threads=2 ``` diff --git a/docs/source/reachy2.mdx b/docs/source/reachy2.mdx index 1b868711a..4b08569db 100644 --- a/docs/source/reachy2.mdx +++ b/docs/source/reachy2.mdx @@ -161,7 +161,7 @@ lerobot-record \ --dataset.private=true \ --dataset.streaming_encoding=true \ --dataset.encoder_threads=2 \ - # --dataset.vcodec=auto \ + # --dataset.camera_encoder.vcodec=auto \ --display_data=true ``` @@ -203,7 +203,7 @@ lerobot-record \ --dataset.private=true \ --dataset.streaming_encoding=true \ --dataset.encoder_threads=2 \ - # --dataset.vcodec=auto \ + # --dataset.camera_encoder.vcodec=auto \ --display_data=true ``` diff --git a/docs/source/smolvla.mdx b/docs/source/smolvla.mdx index bf8a0d2f0..6c63c5d11 100644 --- a/docs/source/smolvla.mdx +++ b/docs/source/smolvla.mdx @@ -108,7 +108,7 @@ lerobot-record \ --dataset.num_episodes=10 \ --dataset.streaming_encoding=true \ --dataset.encoder_threads=2 \ - # --dataset.vcodec=auto \ + # --dataset.camera_encoder.vcodec=auto \ # <- Teleop optional if you want to teleoperate in between episodes \ # --teleop.type=so100_leader \ # --teleop.port=/dev/ttyACM0 \ diff --git a/docs/source/streaming_video_encoding.mdx b/docs/source/streaming_video_encoding.mdx index 40004200e..96e049eb3 100644 --- a/docs/source/streaming_video_encoding.mdx +++ b/docs/source/streaming_video_encoding.mdx @@ -17,9 +17,9 @@ This makes `save_episode()` near-instant (the video is already encoded by the ti | Parameter | CLI Flag | Type | Default | Description | | ----------------------- | --------------------------------- | ------------- | ------------- | ----------------------------------------------------------------- | | `streaming_encoding` | `--dataset.streaming_encoding` | `bool` | `True` | Enable real-time encoding during capture | -| `vcodec` | `--dataset.vcodec` | `str` | `"libsvtav1"` | Video codec. `"auto"` detects best HW encoder | +| `vcodec` | `--dataset.camera_encoder.vcodec` | `str` | `"libsvtav1"` | Video codec. `"auto"` detects best HW encoder | | `encoder_threads` | `--dataset.encoder_threads` | `int \| None` | `None` (auto) | Threads per encoder instance. `None` will leave the vcoded decide | -| `encoder_queue_maxsize` | `--dataset.encoder_queue_maxsize` | `int` | `60` | Max buffered frames per camera (~2s at 30fps). Consumes RAM | +| `encoder_queue_maxsize` | `--dataset.encoder_queue_maxsize` | `int` | `30` | Max buffered frames per camera (~1s at 30fps). Consumes RAM | ## 3. Performance Considerations @@ -48,7 +48,7 @@ This parameter controls how many threads each encoder instance uses internally: ### Backpressure and Frame Dropping -Each camera has a bounded queue (`encoder_queue_maxsize`, default 60 frames). When the encoder can't keep up: +Each camera has a bounded queue (`encoder_queue_maxsize`, default 30 frames). When the encoder can't keep up: 1. The queue fills up (consuming RAM) 2. New frames are **dropped** (not blocked) — the capture loop continues uninterrupted @@ -82,15 +82,15 @@ Use HW encoding when: ### Available HW Encoders -| Encoder | Platform | Hardware | CLI Value | -| ------------------- | ------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------ | -| `h264_videotoolbox` | macOS | Apple Silicon / Intel | `--dataset.vcodec=h264_videotoolbox` | -| `hevc_videotoolbox` | macOS | Apple Silicon / Intel | `--dataset.vcodec=hevc_videotoolbox` | -| `h264_nvenc` | Linux/Windows | NVIDIA GPU | `--dataset.vcodec=h264_nvenc` | -| `hevc_nvenc` | Linux/Windows | NVIDIA GPU | `--dataset.vcodec=hevc_nvenc` | -| `h264_vaapi` | Linux | Intel/AMD GPU | `--dataset.vcodec=h264_vaapi` | -| `h264_qsv` | Linux/Windows | Intel Quick Sync | `--dataset.vcodec=h264_qsv` | -| `auto` | Any | Probes the system for available HW encoders. Falls back to `libsvtav1` if no HW encoder is found | `--dataset.vcodec=auto` | +| Encoder | Platform | Hardware | CLI Value | +| ------------------- | ------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------- | +| `h264_videotoolbox` | macOS | Apple Silicon / Intel | `--dataset.camera_encoder.vcodec=h264_videotoolbox` | +| `hevc_videotoolbox` | macOS | Apple Silicon / Intel | `--dataset.camera_encoder.vcodec=hevc_videotoolbox` | +| `h264_nvenc` | Linux/Windows | NVIDIA GPU | `--dataset.camera_encoder.vcodec=h264_nvenc` | +| `hevc_nvenc` | Linux/Windows | NVIDIA GPU | `--dataset.camera_encoder.vcodec=hevc_nvenc` | +| `h264_vaapi` | Linux | Intel/AMD GPU | `--dataset.camera_encoder.vcodec=h264_vaapi` | +| `h264_qsv` | Linux/Windows | Intel Quick Sync | `--dataset.camera_encoder.vcodec=h264_qsv` | +| `auto` | Any | Probes the system for available HW encoders. Falls back to `libsvtav1` if no HW encoder is found | `--dataset.camera_encoder.vcodec=auto` | > [!NOTE] > In order to use the HW accelerated encoders you might need to upgrade your GPU drivers. @@ -100,15 +100,15 @@ Use HW encoding when: ## 5. Troubleshooting -| Symptom | Likely Cause | Fix | -| ------------------------------------------------------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| System freezes or choppy robot movement or Rerun visualization lag | CPU starved (100% load usage) | Close other apps, reduce encoding throughput, lower `encoder_threads`, use `h264`, use `display_data=False`. If the CPU continues to be at 100% then it might be insufficient for your setup, consider `--dataset.streaming_encoding=false` or HW encoding (`--dataset.vcodec=auto`) | -| "Encoder queue full" warnings or dropped frames in dataset | Encoder can't keep up (Queue overflow) | If CPU is not at 100%: Increase `encoder_threads`, increase `encoder_queue_maxsize` or use HW encoding (`--dataset.vcodec=auto`). | -| High RAM usage | Queue filling faster than encoding | `encoder_threads` too low or CPU insufficient. Reduce `encoder_queue_maxsize` or use HW encoding | -| Large video files | Using HW encoder or H.264 | Expected trade-off. Switch to `libsvtav1` if CPU allows | -| `save_episode()` still slow | `streaming_encoding` is `False` | Set `--dataset.streaming_encoding=true` | -| Encoder thread crash | Codec not available or invalid settings | Check `vcodec` is installed, try `--dataset.vcodec=auto` | -| Recorded dataset is missing frames | CPU/GPU starvation or occasional load spikes | If ~5% of frames are missing, your system is likely overloaded — follow the recommendations above. If fewer frames are missing (~2%), they are probably due to occasional transient load spikes (often at startup) and can be considered expected. | +| Symptom | Likely Cause | Fix | +| ------------------------------------------------------------------ | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| System freezes or choppy robot movement or Rerun visualization lag | CPU starved (100% load usage) | Close other apps, reduce encoding throughput, lower `encoder_threads`, use `h264`, use `display_data=False`. If the CPU continues to be at 100% then it might be insufficient for your setup, consider `--dataset.streaming_encoding=false` or HW encoding (`--dataset.camera_encoder.vcodec=auto`) | +| "Encoder queue full" warnings or dropped frames in dataset | Encoder can't keep up (Queue overflow) | If CPU is not at 100%: Increase `encoder_threads`, increase `encoder_queue_maxsize` or use HW encoding (`--dataset.camera_encoder.vcodec=auto`). | +| High RAM usage | Queue filling faster than encoding | `encoder_threads` too low or CPU insufficient. Reduce `encoder_queue_maxsize` or use HW encoding | +| Large video files | Using HW encoder or H.264 | Expected trade-off. Switch to `libsvtav1` if CPU allows | +| `save_episode()` still slow | `streaming_encoding` is `False` | Set `--dataset.streaming_encoding=true` | +| Encoder thread crash | Codec not available or invalid settings | Check `vcodec` is installed, try `--dataset.camera_encoder.vcodec=auto` | +| Recorded dataset is missing frames | CPU/GPU starvation or occasional load spikes | If ~5% of frames are missing, your system is likely overloaded — follow the recommendations above. If fewer frames are missing (~2%), they are probably due to occasional transient load spikes (often at startup) and can be considered expected. | ## 6. Recommended Configurations @@ -146,7 +146,7 @@ On very constrained systems, streaming encoding may compete too heavily with the # 2camsx 640x480x3 @30fps: Requires some tuning. # Use H.264, disable streaming, consider batching encoding -lerobot-record --dataset.vcodec=h264 --dataset.streaming_encoding=false ... +lerobot-record --dataset.camera_encoder.vcodec=h264 --dataset.streaming_encoding=false ... ``` ## 7. Closing note diff --git a/docs/source/using_dataset_tools.mdx b/docs/source/using_dataset_tools.mdx index f7fc9be20..49247a6c1 100644 --- a/docs/source/using_dataset_tools.mdx +++ b/docs/source/using_dataset_tools.mdx @@ -117,10 +117,10 @@ lerobot-edit-dataset \ --repo_id lerobot/pusht_image \ --operation.type convert_image_to_video \ --operation.output_dir outputs/pusht_video \ - --operation.vcodec libsvtav1 \ - --operation.pix_fmt yuv420p \ - --operation.g 2 \ - --operation.crf 30 + --operation.camera_encoder.vcodec libsvtav1 \ + --operation.camera_encoder.pix_fmt yuv420p \ + --operation.camera_encoder.g 2 \ + --operation.camera_encoder.crf 30 # Convert only specific episodes lerobot-edit-dataset \ @@ -147,11 +147,7 @@ lerobot-edit-dataset \ **Parameters:** - `output_dir`: Custom output directory (optional - by default uses `new_repo_id` or `{repo_id}_video`) -- `vcodec`: Video codec to use - options: `h264`, `hevc`, `libsvtav1` (default: `libsvtav1`) -- `pix_fmt`: Pixel format - options: `yuv420p`, `yuv444p` (default: `yuv420p`) -- `g`: Group of pictures (GOP) size - lower values give better quality but larger files (default: 2) -- `crf`: Constant rate factor - lower values give better quality but larger files, 0 is lossless (default: 30) -- `fast_decode`: Fast decode tuning option (default: 0) +- `camera_encoder`: Video encoder settings — all sub-fields accessible via `--operation.camera_encoder.. See [Video Encoding Parameters](./video_encoding_parameters) for more details. - `episode_indices`: List of specific episodes to convert (default: all episodes) - `num_workers`: Number of parallel workers for processing (default: 4) diff --git a/docs/source/video_encoding_parameters.mdx b/docs/source/video_encoding_parameters.mdx new file mode 100644 index 000000000..0b5b99b2b --- /dev/null +++ b/docs/source/video_encoding_parameters.mdx @@ -0,0 +1,117 @@ +# Video encoding parameters + +When video storage is enabled, LeRobot stores each camera stream as an **MP4** file instead of saving one image file per timestep. Video encoding compresses across time, which usually cuts dataset size and I/O compared to a pile of PNG, while keeping MP4 — a format every player and loader understands. + +Encoding frames into an MP4 is a full FFmpeg pipeline: choice of encoder, pixel format, GOP/keyframes, quality vs. speed, and optional extra encoder flags. Most of these knobs are user-tunable through `camera_encoder`, a nested `VideoEncoderConfig` (`lerobot.configs.video.VideoEncoderConfig`) passed through PyAV. + +You can set these parameters from the CLI with `--dataset.camera_encoder.` (e.g. with `lerobot-record` or `lerobot-rollout`). The same block applies to every camera video stream in that run. + + + Video storage must be on for `camera_encoder` to have any effect — + `use_videos=True` in Python APIs, or `--dataset.video=true` on the CLI (the + recording default). With video off, inputs stay as images and `camera_encoder` + is ignored. + + +For details on **when** frames are written vs. encoded (streaming vs. post-episode), queues, and other top-level `--dataset.*` switches, see [Streaming Video Encoding](./streaming_video_encoding). For an encoding-parameter comparison and experiments, see the [video-benchmark Space](https://huggingface.co/spaces/lerobot/video-benchmark). + +--- + +## Example + +```bash +lerobot-record \ + --robot.type=so100_follower \ + --robot.port=/dev/tty.usbmodem58760431541 \ + --robot.cameras="{laptop: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \ + --robot.id=black \ + --teleop.type=so100_leader \ + --teleop.port=/dev/tty.usbmodem58760431551 \ + --teleop.id=blue \ + --dataset.repo_id=/ \ + --dataset.num_episodes=2 \ + --dataset.single_task="Grab the cube" \ + --dataset.streaming_encoding=true \ + --dataset.encoder_threads=2 \ + --dataset.camera_encoder.vcodec=h264 \ + --dataset.camera_encoder.preset=fast \ + --dataset.camera_encoder.extra_options={"tune": "film", "profile:v": "high", "bf": 2} \ + --display_data=true +``` + +--- + +## Tuning parameters + + +The defaults are tuned to balance **compression ratio**, **visual quality**, and **decoding/seek speed** for typical robotics datasets. Changing them can affect both recording (CPU load, frame drops) and training (decoding throughput, image quality). + +Only override these parameters if you have a specific reason to, and measure the impact on your pipeline before relying on the new settings. + + + +All flags below are prefixed with `--dataset.camera_encoder.` on the CLI. + +| Parameter | Type | Default | Description | +| --------------- | ---------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `vcodec` | `str` | `"libsvtav1"` | Video codec name. `"auto"` picks the first available hardware encoder from a fixed preference list, falling back to `libsvtav1`. | +| `pix_fmt` | `str` | `"yuv420p"` | Output pixel format. Must be supported by the chosen codec in your FFmpeg build. | +| `g` | `int` | `2` | GOP size — a keyframe every `g` frames. Emitted as FFmpeg option `g`. | +| `crf` | `int` or `float` | `30` | Abstract quality value, mapped per codec (see the [mapping](#mapping-videoencoderconfig--ffmpeg-options) below). Lower → higher quality / larger output where the mapping is monotone. | +| `preset` | `int` or `str` | `12` \* | Encoder speed preset; meaning depends on the codec.
\* When unset and `vcodec=libsvtav1`, LeRobot defaults to `12`. | +| `fast_decode` | `int` | `0` | `libsvtav1`: `0–2`, passed via `svtav1-params`.
`h264` / `hevc` (software): if `>0`, sets `tune=fastdecode`.
Other codecs: usually unused. | +| `video_backend` | `str` | `"pyav"` | Only `"pyav"` is currently implemented for video encoding. | +| `extra_options` | `dict` | `{}` | Extra FFmpeg or codec specific options merged after the structured fields above. Cannot override keys already set by those fields. | + +--- + +## Persistence in dataset metadata + +After the first episode of a video stream is encoded, the encoder configuration is **persisted into the dataset metadata** (`meta/info.json`) under each video feature, alongside the values probed from the file itself. For a video feature `observation.images.`, the layout in `info.json` is: + +```json +{ + "features": { + "observation.images.laptop": { + "dtype": "video", + "shape": [480, 640, 3], + "info": { + "video.height": 480, + "video.width": 640, + "video.codec": "h264", + "video.pix_fmt": "yuv420p", + "video.fps": 30, + "video.channels": 3, + "video.is_depth_map": false, + "video.g": 2, + "video.crf": 30, + "video.preset": "fast", + "video.fast_decode": 0, + "video.video_backend": "pyav", + "video.extra_options": { "tune": "film", "profile:v": "high", "bf": 2 } + } + } + } +} +``` + +Two sources contribute to the `info` block: + +- **Stream-derived** (read back from the encoded MP4 with PyAV): `video.height`, `video.width`, `video.codec`, `video.pix_fmt`, `video.fps`, `video.channels`, `video.is_depth_map`, plus `audio.*` if an audio stream is present. +- **Encoder-derived** (taken from `VideoEncoderConfig`): `video.g`, `video.crf`, `video.preset`, `video.fast_decode`, `video.video_backend`, `video.extra_options`. + + + This block is populated **once**, from the **first** episode. It assumes every + episode in the dataset was encoded with the same `camera_encoder`. Changing + encoder settings partway through a recording is not supported — the + `info.json` will only reflect the parameters used for the first episode. + + +--- + +## Merging datasets + +When aggregating datasets with `merge_datasets`, video files are concatenated as-is (no re-encoding), and encoder fields in `info.json` are merged per-key: + +- **Stream-derived fields must match** across sources: `video.codec`, `video.pix_fmt`, `video.height`, `video.width`, `video.fps`. Otherwise FFmpeg's concat demuxer fails. +- **Encoder-tuning fields are merged loosely**: `video.g`, `video.crf`, `video.preset`, `video.fast_decode`, `video.extra_options`. If every source agrees, the value is kept; if not, it's set to `null` (or `{}` for `video.extra_options`) and a warning is logged. diff --git a/src/lerobot/configs/__init__.py b/src/lerobot/configs/__init__.py index ab74c3cd3..c3fe246cd 100644 --- a/src/lerobot/configs/__init__.py +++ b/src/lerobot/configs/__init__.py @@ -31,6 +31,12 @@ from .types import ( PolicyFeature, RTCAttentionSchedule, ) +from .video import ( + VALID_VIDEO_CODECS, + VIDEO_ENCODER_INFO_KEYS, + VideoEncoderConfig, + camera_encoder_defaults, +) __all__ = [ # Types @@ -46,4 +52,10 @@ __all__ = [ "PeftConfig", "PreTrainedConfig", "WandBConfig", + "VideoEncoderConfig", + # Defaults + "camera_encoder_defaults", + # Constants + "VALID_VIDEO_CODECS", + "VIDEO_ENCODER_INFO_KEYS", ] diff --git a/src/lerobot/configs/dataset.py b/src/lerobot/configs/dataset.py index e3e17e62b..d5c6fa312 100644 --- a/src/lerobot/configs/dataset.py +++ b/src/lerobot/configs/dataset.py @@ -14,10 +14,12 @@ """Shared dataset recording configuration used by both ``lerobot-record`` and ``lerobot-rollout``.""" -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import datetime from pathlib import Path +from .video import VideoEncoderConfig, camera_encoder_defaults + @dataclass class DatasetRecordConfig: @@ -55,10 +57,9 @@ class DatasetRecordConfig: # Number of episodes to record before batch encoding videos # Set to 1 for immediate encoding (default behavior), or higher for batched encoding video_encoding_batch_size: int = 1 - # Video codec for encoding videos. Options: 'h264', 'hevc', 'libsvtav1', 'auto', - # or hardware-specific: 'h264_videotoolbox', 'h264_nvenc', 'h264_vaapi', 'h264_qsv'. - # Use 'auto' to auto-detect the best available hardware encoder. - vcodec: str = "libsvtav1" + # Video encoder settings for camera MP4s (codec, quality, GOP, etc.). Tuned via CLI nested keys, + # e.g. ``--dataset.camera_encoder.vcodec=h264`` (see ``VideoEncoderConfig``). + camera_encoder: VideoEncoderConfig = field(default_factory=camera_encoder_defaults) # Enable streaming video encoding: encode frames in real-time during capture instead # of writing PNG images first. Makes save_episode() near-instant. More info in the documentation: https://huggingface.co/docs/lerobot/streaming_video_encoding streaming_encoding: bool = False diff --git a/src/lerobot/configs/default.py b/src/lerobot/configs/default.py index b1eebba94..b809e71d9 100644 --- a/src/lerobot/configs/default.py +++ b/src/lerobot/configs/default.py @@ -17,7 +17,7 @@ from dataclasses import dataclass, field from lerobot.transforms import ImageTransformsConfig -from lerobot.utils.import_utils import get_safe_default_codec +from lerobot.utils.import_utils import get_safe_default_video_backend @dataclass @@ -34,7 +34,7 @@ class DatasetConfig: image_transforms: ImageTransformsConfig = field(default_factory=ImageTransformsConfig) revision: str | None = None use_imagenet_stats: bool = True - video_backend: str = field(default_factory=get_safe_default_codec) + video_backend: str = field(default_factory=get_safe_default_video_backend) # When True, video frames are returned as uint8 tensors (0-255) instead of float32 (0.0-1.0). # This reduces memory and speeds up DataLoader IPC. The training pipeline handles the conversion. return_uint8: bool = False diff --git a/src/lerobot/configs/eval.py b/src/lerobot/configs/eval.py index f2a1d3065..c285025ad 100644 --- a/src/lerobot/configs/eval.py +++ b/src/lerobot/configs/eval.py @@ -18,8 +18,8 @@ from logging import getLogger from pathlib import Path from lerobot import envs, policies # noqa: F401 -from lerobot.configs import parser +from . import parser from .default import EvalConfig from .policies import PreTrainedConfig diff --git a/src/lerobot/configs/rewards.py b/src/lerobot/configs/rewards.py index d495160bf..a53d5a417 100644 --- a/src/lerobot/configs/rewards.py +++ b/src/lerobot/configs/rewards.py @@ -27,12 +27,13 @@ from huggingface_hub import hf_hub_download from huggingface_hub.constants import CONFIG_NAME from huggingface_hub.errors import HfHubHTTPError -from lerobot.configs.types import PolicyFeature from lerobot.optim.optimizers import OptimizerConfig from lerobot.optim.schedulers import LRSchedulerConfig from lerobot.utils.device_utils import auto_select_torch_device, is_torch_device_available from lerobot.utils.hub import HubMixin +from .types import PolicyFeature + T = TypeVar("T", bound="RewardModelConfig") logger = logging.getLogger(__name__) diff --git a/src/lerobot/configs/train.py b/src/lerobot/configs/train.py index c5b4ff5f5..55498d3ac 100644 --- a/src/lerobot/configs/train.py +++ b/src/lerobot/configs/train.py @@ -25,11 +25,11 @@ from huggingface_hub import hf_hub_download from huggingface_hub.errors import HfHubHTTPError from lerobot import envs -from lerobot.configs import parser from lerobot.optim import LRSchedulerConfig, OptimizerConfig from lerobot.utils.hub import HubMixin from lerobot.utils.sample_weighting import SampleWeightingConfig +from . import parser from .default import DatasetConfig, EvalConfig, PeftConfig, WandBConfig from .policies import PreTrainedConfig from .rewards import RewardModelConfig diff --git a/src/lerobot/configs/video.py b/src/lerobot/configs/video.py new file mode 100644 index 000000000..bf2471453 --- /dev/null +++ b/src/lerobot/configs/video.py @@ -0,0 +1,235 @@ +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Note: We subclass str so that serialization is straightforward +# https://stackoverflow.com/questions/24481852/serialising-an-enum-member-to-json + +"""Video encoder configurations.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field +from typing import Any + +from lerobot.utils.import_utils import require_package + +logger = logging.getLogger(__name__) + +# List of hardware encoders to probe for auto-selection. Availability depends on the platform and the chosen video backend. +# Determines the order of preference for auto-selection when vcodec="auto" is used. +HW_VIDEO_CODECS = [ + "h264_videotoolbox", # macOS + "hevc_videotoolbox", # macOS + "h264_nvenc", # NVIDIA GPU + "hevc_nvenc", # NVIDIA GPU + "h264_vaapi", # Linux Intel/AMD + "h264_qsv", # Intel Quick Sync +] +VALID_VIDEO_CODECS: frozenset[str] = frozenset({"h264", "hevc", "libsvtav1", "auto", *HW_VIDEO_CODECS}) +# Aliases for legacy video codec names. +VIDEO_CODECS_ALIASES: dict[str, str] = {"av1": "libsvtav1"} + + +LIBSVTAV1_DEFAULT_PRESET: int = 12 + +# Keys persisted under ``features[*]["info"]`` as ``video.`` (from :class:`VideoEncoderConfig`). +# ``vcodec``` and ``pix_fmt`` are derived from the video stream directly. +VIDEO_ENCODER_INFO_FIELD_NAMES: frozenset[str] = frozenset( + {"g", "crf", "preset", "fast_decode", "extra_options", "video_backend"} +) +VIDEO_ENCODER_INFO_KEYS: frozenset[str] = frozenset( + f"video.{name}" for name in VIDEO_ENCODER_INFO_FIELD_NAMES +) + + +@dataclass +class VideoEncoderConfig: + """Video encoder configuration. + + Attributes: + vcodec: Video encoder name. ``"auto"`` is resolved during + construction (HW encoder if available, else ``libsvtav1``). + pix_fmt: Pixel format (e.g. ``"yuv420p"``). + g: GOP size (keyframe interval). + crf: Quality level — mapped to the native quality parameter of the + codec (``crf`` for software, ``qp`` for NVENC/VAAPI, + ``q:v`` for VideoToolbox, ``global_quality`` for QSV). + preset: Speed/quality preset. Accepted type is per-codec. + fast_decode: Fast-decode tuning. For ``libsvtav1`` this is a level (0-2) + embedded in ``svtav1-params``. For ``h264`` and ``hevc`` non-zero values + set ``tune=fastdecode``. Ignored for other codecs. + video_backend: Python to be used for encoding. Only ``"pyav"`` + is currently supported. + extra_options: Free-form dictionary of additional video encoder options + (e.g. ``{"tune": "film", "profile:v": "high", "bf": 2}``). + """ + + vcodec: str = "libsvtav1" # TODO(CarolinePascal): rename to codec ? + pix_fmt: str = "yuv420p" + g: int | None = 2 + crf: int | float | None = 30 + preset: int | str | None = None + fast_decode: int = 0 + # TODO(CarolinePascal): add torchcodec support + find a way to unify the + # two backends (encoding and decoding). + video_backend: str = "pyav" + extra_options: dict[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + self.resolve_vcodec() + # Empty-constructor ergonomics: ``VideoEncoderConfig()`` must "just work". + if self.preset is None and self.vcodec == "libsvtav1": + self.preset = LIBSVTAV1_DEFAULT_PRESET + self.validate() + + @classmethod + def from_video_info(cls, video_info: dict | None) -> VideoEncoderConfig: + """Reconstruct a :class:`VideoEncoderConfig` from a video feature's ``info`` block. + Missing or ``None`` values fall back to the class defaults. + """ + video_info = video_info or {} + kwargs: dict[str, Any] = {} + + for src_key, dst_field in (("video.codec", "vcodec"), ("video.pix_fmt", "pix_fmt")): + value = video_info.get(src_key) + if value is not None: + kwargs[dst_field] = value + + for field_name in VIDEO_ENCODER_INFO_FIELD_NAMES: + value = video_info.get(f"video.{field_name}") + if value is None: + continue + # Persisted as ``{}`` after merges with disagreeing sources — treat as default. + if field_name == "extra_options" and not value: + continue + kwargs[field_name] = value + + return cls(**kwargs) + + def detect_available_encoders(self, encoders: list[str] | str) -> list[str]: + """Return the subset of available encoders based on the specified video backend. + + Args: + encoders: List of encoder names to detect. If a string, it is converted to a list. + Returns: + List of available encoder names. If the video backend is not "pyav", returns an empty list. + """ + if self.video_backend == "pyav": + require_package("av", extra="dataset") + from lerobot.datasets import detect_available_encoders_pyav + + return detect_available_encoders_pyav(encoders) + return [] + + def validate(self) -> None: + """Validate the video encoder configuration.""" + if self.video_backend == "pyav": + require_package("av", extra="dataset") + from lerobot.datasets import check_video_encoder_parameters_pyav + + check_video_encoder_parameters_pyav(self.vcodec, self.pix_fmt, self.get_codec_options()) + + def resolve_vcodec(self) -> None: + """Check ``vcodec`` and, when it is ``"auto"``, pick a concrete encoder. + + For ``"auto"``, the first hardware encoder in the preference list that is available is chosen; if none are available, ``libsvtav1`` is used. If the + resolved codec (explicit or after auto-selection) is not available, raises ``ValueError``. + + Stream-derived canonical codec names listed in :data:`VIDEO_CODECS_ALIASES` are + rewritten to their corresponding encoder name (e.g. ``"av1"`` → ``"libsvtav1"``). + """ + self.vcodec = VIDEO_CODECS_ALIASES.get(self.vcodec, self.vcodec) + if self.vcodec not in VALID_VIDEO_CODECS: + raise ValueError(f"Invalid vcodec '{self.vcodec}'. Must be one of: {sorted(VALID_VIDEO_CODECS)}") + if self.vcodec == "auto": + available = self.detect_available_encoders(HW_VIDEO_CODECS) + for encoder in HW_VIDEO_CODECS: + if encoder in available: + logger.info(f"Auto-selected video codec: {encoder}") + self.vcodec = encoder + return + logger.warning("No hardware encoder available, falling back to software encoder 'libsvtav1'") + self.vcodec = "libsvtav1" + + if self.detect_available_encoders(self.vcodec): + logger.info(f"Using video codec: {self.vcodec}") + return + raise ValueError(f"Unsupported video codec: {self.vcodec} with video backend {self.video_backend}") + + def get_codec_options( + self, encoder_threads: int | None = None, as_strings: bool = False + ) -> dict[str, Any]: + """Translate the tuning fields to codec-specific options. + + ``VideoEncoderConfig.extra_options`` are merged last but never override a structured field. + + Args: + encoder_threads: Number of encoder threads set globally for all VideoEncoderConfigs. + For libsvtav1, this is mapped to ``lp`` via ``svtav1-params``. + For h264/hevc, this is mapped to ``threads``. + Hardware encoders ignore this parameter. + as_strings: If ``True``, casts values to strings. + """ + opts: dict[str, Any] = {} + + def set_if(key: str, value: Any) -> None: + if value is not None: + opts[key] = value if not as_strings else str(value) + + # GOP size is not a codec-specific option, so it is always set. + set_if("g", self.g) + + if self.vcodec == "libsvtav1": + set_if("crf", self.crf) + set_if("preset", self.preset) + svtav1_parts: list[str] = [] + if self.fast_decode is not None: + svtav1_parts.append(f"fast-decode={max(0, min(2, self.fast_decode))}") + if encoder_threads is not None: + svtav1_parts.append(f"lp={encoder_threads}") + if svtav1_parts: + opts["svtav1-params"] = ":".join(svtav1_parts) + elif self.vcodec in ("h264", "hevc"): + set_if("crf", self.crf) + set_if("preset", self.preset) + if self.fast_decode: + opts["tune"] = "fastdecode" + set_if("threads", encoder_threads) + elif self.vcodec in ("h264_videotoolbox", "hevc_videotoolbox"): + if self.crf is not None: + opts["q:v"] = max(1, min(100, 100 - self.crf * 2)) + elif self.vcodec in ("h264_nvenc", "hevc_nvenc"): + opts["rc"] = 0 + set_if("qp", self.crf) + set_if("preset", self.preset) + elif self.vcodec == "h264_vaapi": + set_if("qp", self.crf) + elif self.vcodec == "h264_qsv": + set_if("global_quality", self.crf) + set_if("preset", self.preset) + else: + set_if("crf", self.crf) + set_if("preset", self.preset) + + # Extra options are merged last but never override structured fields (values are kept as given). + for k, v in self.extra_options.items(): + if k not in opts: + set_if(k, v) + + return opts + + +def camera_encoder_defaults() -> VideoEncoderConfig: + """Return a :class:`VideoEncoderConfig` with RGB-camera defaults.""" + return VideoEncoderConfig() diff --git a/src/lerobot/datasets/__init__.py b/src/lerobot/datasets/__init__.py index 6c42959a5..b51ef0222 100644 --- a/src/lerobot/datasets/__init__.py +++ b/src/lerobot/datasets/__init__.py @@ -40,6 +40,7 @@ from .io_utils import load_episodes, write_stats from .lerobot_dataset import LeRobotDataset from .multi_dataset import MultiLeRobotDataset from .pipeline_features import aggregate_pipeline_dataset_features, create_initial_features +from .pyav_utils import check_video_encoder_parameters_pyav, detect_available_encoders_pyav from .sampler import EpisodeAwareSampler from .streaming_dataset import StreamingLeRobotDataset from .utils import DEFAULT_EPISODES_PATH, create_lerobot_dataset_card @@ -59,6 +60,8 @@ __all__ = [ "MultiLeRobotDataset", "StreamingLeRobotDataset", "VideoEncodingManager", + "check_video_encoder_parameters_pyav", + "detect_available_encoders_pyav", "add_features", "aggregate_datasets", "aggregate_pipeline_dataset_features", diff --git a/src/lerobot/datasets/aggregate.py b/src/lerobot/datasets/aggregate.py index 90fc8f583..5db3f934d 100644 --- a/src/lerobot/datasets/aggregate.py +++ b/src/lerobot/datasets/aggregate.py @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy import logging import shutil from pathlib import Path @@ -23,9 +24,11 @@ import datasets import pandas as pd import tqdm +from lerobot.configs import VIDEO_ENCODER_INFO_KEYS + from .compute_stats import aggregate_stats from .dataset_metadata import LeRobotDatasetMetadata -from .feature_utils import get_hf_features_from_features +from .feature_utils import features_equal_for_merge, get_hf_features_from_features from .io_utils import ( get_file_size_in_mb, get_parquet_file_size_in_mb, @@ -46,11 +49,54 @@ from .utils import ( from .video_utils import concatenate_video_files, get_video_duration_in_s +def merge_video_feature_info_for_aggregate(all_metadata: list[LeRobotDatasetMetadata]) -> dict[str, dict]: + """Create a merged video feature info dictionary for aggregation. The video encoder info is merged field-by-field: each key is kept only when every source agrees; otherwise that key is set to ``null`` (or ``{}`` for ``video.extra_options``) and a warning is logged. + + Args: + all_metadata: List of LeRobotDatasetMetadata objects to merge. + + Returns: + dict: A dictionary of merged video feature info. + """ + merged_info = copy.deepcopy(all_metadata[0].features) + video_keys = [k for k in merged_info if merged_info[k].get("dtype") == "video"] + + for vk in video_keys: + video_infos = [m.features.get(vk, {}).get("info") or {} for m in all_metadata] + base_video_info = video_infos[0] + + merged_encoder_info: dict = {} + fallback_keys: list[str] = [] + for info_key in VIDEO_ENCODER_INFO_KEYS: + values = [info.get(info_key, None) for info in video_infos] + first_value = values[0] + all_match = all(v == first_value for v in values[1:]) + + if all_match: + merged_encoder_info[info_key] = first_value + else: + fallback_keys.append(info_key) + merged_encoder_info[info_key] = {} if info_key == "video.extra_options" else None + + if fallback_keys: + logging.warning( + f"Merging heterogeneous or incomplete video encoder metadata for feature {vk}. " + f"Setting these keys to null: {fallback_keys}.", + ) + + merged_info[vk]["info"] = {**base_video_info, **merged_encoder_info} + # TODO(CarolinePascal): make this variable once we have support for other video backends. + merged_info[vk]["info"]["video.video_backend"] = "pyav" + + return merged_info + + def validate_all_metadata(all_metadata: list[LeRobotDatasetMetadata]): """Validates that all dataset metadata have consistent properties. Ensures all datasets have the same fps, robot_type, and features to guarantee compatibility when aggregating them into a single dataset. + Video encoder info is not considered for validation but is merged during aggregation in ``merge_video_feature_info_for_aggregate``. Args: all_metadata: List of LeRobotDatasetMetadata objects to validate. @@ -74,7 +120,7 @@ def validate_all_metadata(all_metadata: list[LeRobotDatasetMetadata]): raise ValueError( f"Same robot_type is expected, but got robot_type={meta.robot_type} instead of {robot_type}." ) - if features != meta.features: + if not features_equal_for_merge(features, meta.features): raise ValueError( f"Same features is expected, but got features={meta.features} instead of {features}." ) @@ -274,7 +320,8 @@ def aggregate_datasets( LeRobotDatasetMetadata(repo_id, root=root) for repo_id, root in zip(repo_ids, roots, strict=False) ] ) - fps, robot_type, features = validate_all_metadata(all_metadata) + fps, robot_type, _ = validate_all_metadata(all_metadata) + features = merge_video_feature_info_for_aggregate(all_metadata) video_keys = [key for key in features if features[key]["dtype"] == "video"] dst_meta = LeRobotDatasetMetadata.create( @@ -332,7 +379,6 @@ def aggregate_videos(src_meta, dst_meta, videos_idx, video_files_size_in_mb, chu videos_idx: Dictionary tracking video chunk and file indices. video_files_size_in_mb: Maximum size for video files in MB (defaults to DEFAULT_VIDEO_FILE_SIZE_IN_MB) chunk_size: Maximum number of files per chunk (defaults to DEFAULT_CHUNK_SIZE) - Returns: dict: Updated videos_idx with current chunk and file indices. """ @@ -414,9 +460,11 @@ def aggregate_videos(src_meta, dst_meta, videos_idx, video_files_size_in_mb, chu current_dst_duration = dst_file_durations.get(dst_key, 0) videos_idx[key]["src_to_offset"][(src_chunk_idx, src_file_idx)] = current_dst_duration videos_idx[key]["src_to_dst"][(src_chunk_idx, src_file_idx)] = dst_key + # TODO(CarolinePascal): Move the check before the loop to avoid failing in the middle + add possibility to re-encode the video if the check fails concatenate_video_files( [dst_path, src_path], dst_path, + compatibility_check=True, ) # Update duration of this destination file dst_file_durations[dst_key] = current_dst_duration + src_duration diff --git a/src/lerobot/datasets/dataset_metadata.py b/src/lerobot/datasets/dataset_metadata.py index b404ddb18..3c58774c3 100644 --- a/src/lerobot/datasets/dataset_metadata.py +++ b/src/lerobot/datasets/dataset_metadata.py @@ -24,6 +24,7 @@ import pyarrow as pa import pyarrow.parquet as pq from huggingface_hub import snapshot_download +from lerobot.configs import VideoEncoderConfig from lerobot.utils.constants import DEFAULT_FEATURES, HF_LEROBOT_HOME, HF_LEROBOT_HUB_CACHE from lerobot.utils.feature_utils import _validate_feature_names from lerobot.utils.utils import flatten_dict @@ -534,10 +535,23 @@ class LeRobotDatasetMetadata: self.stats = aggregate_stats([self.stats, episode_stats]) if self.stats is not None else episode_stats write_stats(self.stats, self.root) - def update_video_info(self, video_key: str | None = None) -> None: - """ + def update_video_info( + self, + video_key: str | None = None, + camera_encoder: VideoEncoderConfig | None = None, + ) -> None: + """Populate per-feature video info in ``info.json``. + Warning: this function writes info from first episode videos, implicitly assuming that all videos have been encoded the same way. Also, this means it assumes the first episode exists. + + Args: + video_key: If provided, only update this video key. Otherwise update + all video keys in the dataset. + camera_encoder: Encoder configuration used to produce the + videos. When provided, its fields are recorded as + ``video.`` entries alongside the stream-derived + ``video.*`` entries (see :func:`get_video_info`). """ if video_key is not None and video_key not in self.video_keys: raise ValueError(f"Video key {video_key} not found in dataset") @@ -546,7 +560,7 @@ class LeRobotDatasetMetadata: for key in video_keys: if not self.features[key].get("info", None): video_path = self.root / self.video_path.format(video_key=key, chunk_index=0, file_index=0) - self.info.features[key]["info"] = get_video_info(video_path) + self.info.features[key]["info"] = get_video_info(video_path, camera_encoder=camera_encoder) def update_chunk_settings( self, diff --git a/src/lerobot/datasets/dataset_tools.py b/src/lerobot/datasets/dataset_tools.py index 46dd9bff2..489914fbc 100644 --- a/src/lerobot/datasets/dataset_tools.py +++ b/src/lerobot/datasets/dataset_tools.py @@ -36,6 +36,7 @@ import pyarrow.parquet as pq import torch from tqdm import tqdm +from lerobot.configs import VideoEncoderConfig, camera_encoder_defaults from lerobot.utils.constants import ACTION, HF_LEROBOT_HOME, OBS_IMAGE, OBS_STATE from lerobot.utils.utils import flatten_dict @@ -62,7 +63,10 @@ from .utils import ( DEFAULT_EPISODES_PATH, update_chunk_file_indices, ) -from .video_utils import encode_video_frames, get_video_info +from .video_utils import ( + encode_video_frames, + get_video_info, +) def _load_episode_with_stats(src_dataset: LeRobotDataset, episode_idx: int) -> dict: @@ -95,6 +99,11 @@ def delete_episodes( ) -> LeRobotDataset: """Delete episodes from a LeRobotDataset and create a new dataset. + Video segments that need re-encoding (because the source file mixes kept and + deleted episodes) are re-encoded with the source dataset's existing encoder + settings — read back from ``meta/info.json`` — so the output dataset stays + consistent with its own metadata. + Args: dataset: The source LeRobotDataset. episode_indices: List of episode indices to delete. @@ -157,6 +166,11 @@ def split_dataset( ) -> dict[str, LeRobotDataset]: """Split a LeRobotDataset into multiple smaller datasets. + Video segments that need re-encoding (because the source file mixes episodes + that fall into different splits) are re-encoded with the source dataset's + existing encoder settings — read back from ``meta/info.json`` — so each + output split stays consistent with its own metadata. + Args: dataset: The source LeRobotDataset to split. splits: Either a dict mapping split names to episode indices, or a dict mapping @@ -578,8 +592,7 @@ def _keep_episodes_from_video_with_av( output_path: Path, episodes_to_keep: list[tuple[int, int]], fps: float, - vcodec: str = "libsvtav1", - pix_fmt: str = "yuv420p", + camera_encoder: VideoEncoderConfig, ) -> None: """Keep only specified episodes from a video file using PyAV. @@ -593,8 +606,7 @@ def _keep_episodes_from_video_with_av( Ranges are half-open intervals: [start_frame, end_frame), where start_frame is inclusive and end_frame is exclusive. fps: Frame rate of the video. - vcodec: Video codec to use for encoding. - pix_fmt: Pixel format for output video. + camera_encoder: Video encoder settings used to re-encode the kept frames. """ from fractions import Fraction @@ -619,12 +631,13 @@ def _keep_episodes_from_video_with_av( # Convert fps to Fraction for PyAV compatibility. fps_fraction = Fraction(fps).limit_denominator(1000) - v_out = out.add_stream(vcodec, rate=fps_fraction) + codec_options = camera_encoder.get_codec_options(as_strings=True) + v_out = out.add_stream(camera_encoder.vcodec, rate=fps_fraction, options=codec_options) # PyAV type stubs don't distinguish video streams from audio/subtitle streams. v_out.width = v_in.codec_context.width v_out.height = v_in.codec_context.height - v_out.pix_fmt = pix_fmt + v_out.pix_fmt = camera_encoder.pix_fmt # Set time_base to match the frame rate for proper timestamp handling. v_out.time_base = Fraction(1, int(fps)) @@ -687,14 +700,14 @@ def _copy_and_reindex_videos( src_dataset: LeRobotDataset, dst_meta: LeRobotDatasetMetadata, episode_mapping: dict[int, int], - vcodec: str = "libsvtav1", - pix_fmt: str = "yuv420p", ) -> dict[int, dict]: """Copy and filter video files, only re-encoding files with deleted episodes. For video files that only contain kept episodes, we copy them directly. For files with mixed kept/deleted episodes, we use PyAV filters to efficiently - re-encode only the desired segments. + re-encode only the desired segments. The encoder used for re-encoding is + derived per video key from the source dataset's ``meta/info.json`` so the + destination metadata keeps describing the videos accurately. Args: src_dataset: Source dataset to copy from @@ -711,6 +724,9 @@ def _copy_and_reindex_videos( for video_key in src_dataset.meta.video_keys: logging.info(f"Processing videos for {video_key}") + camera_encoder = VideoEncoderConfig.from_video_info( + src_dataset.meta.info.features.get(video_key, {}).get("info") + ) if dst_meta.video_path is None: raise ValueError("Destination metadata has no video_path defined") @@ -792,8 +808,7 @@ def _copy_and_reindex_videos( dst_video_path, episodes_to_keep_ranges, src_dataset.meta.fps, - vcodec, - pix_fmt, + camera_encoder, ) cumulative_ts = 0.0 @@ -1264,11 +1279,7 @@ def _estimate_frame_size_via_calibration( episode_indices: list[int], temp_dir: Path, fps: int, - vcodec: str, - pix_fmt: str, - g: int, - crf: int, - fast_decode: int, + camera_encoder: VideoEncoderConfig, num_calibration_frames: int = 30, ) -> float: """Estimate MB per frame by encoding a small calibration sample. @@ -1282,11 +1293,7 @@ def _estimate_frame_size_via_calibration( episode_indices: List of episode indices being processed. temp_dir: Temporary directory for calibration files. fps: Frames per second for video encoding. - vcodec: Video codec (libsvtav1, h264, hevc). - pix_fmt: Pixel format (yuv420p, etc.). - g: GOP size (group of pictures). - crf: Constant Rate Factor (quality). - fast_decode: Fast decode tuning parameter. + camera_encoder: Video encoder settings used for calibration encoding. num_calibration_frames: Number of frames to use for calibration (default: 30). Returns: @@ -1322,11 +1329,7 @@ def _estimate_frame_size_via_calibration( imgs_dir=calibration_dir, video_path=calibration_video_path, fps=fps, - vcodec=vcodec, - pix_fmt=pix_fmt, - g=g, - crf=crf, - fast_decode=fast_decode, + camera_encoder=camera_encoder, overwrite=True, ) @@ -1644,11 +1647,7 @@ def convert_image_to_video_dataset( dataset: LeRobotDataset, output_dir: Path | None = None, repo_id: str | None = None, - vcodec: str = "libsvtav1", - pix_fmt: str = "yuv420p", - g: int = 2, - crf: int = 30, - fast_decode: int = 0, + camera_encoder: VideoEncoderConfig | None = None, episode_indices: list[int] | None = None, num_workers: int = 4, max_episodes_per_batch: int | None = None, @@ -1663,11 +1662,8 @@ def convert_image_to_video_dataset( dataset: The source LeRobot dataset with images output_dir: Root directory where the edited dataset will be stored. If not specified, defaults to $HF_LEROBOT_HOME/repo_id. Equivalent to new_root in EditDatasetConfig. repo_id: Edited dataset identifier. Equivalent to new_repo_id in EditDatasetConfig. - vcodec: Video codec (default: libsvtav1) - pix_fmt: Pixel format (default: yuv420p) - g: Group of pictures size (default: 2) - crf: Constant rate factor (default: 30) - fast_decode: Fast decode tuning (default: 0) + camera_encoder: Video encoder settings + (``None`` uses :func:`~lerobot.configs.camera_encoder_defaults`). episode_indices: List of episode indices to convert (None = all episodes) num_workers: Number of threads for parallel processing (default: 4) max_episodes_per_batch: Maximum episodes per video batch to avoid memory issues (None = no limit) @@ -1676,6 +1672,9 @@ def convert_image_to_video_dataset( Returns: New LeRobotDataset with images encoded as videos """ + if camera_encoder is None: + camera_encoder = camera_encoder_defaults() + # Check that it's an image dataset if len(dataset.meta.video_keys) > 0: raise ValueError( @@ -1699,7 +1698,10 @@ def convert_image_to_video_dataset( logging.info( f"Converting {len(episode_indices)} episodes with {len(img_keys)} cameras from {dataset.repo_id}" ) - logging.info(f"Video codec: {vcodec}, pixel format: {pix_fmt}, GOP: {g}, CRF: {crf}") + logging.info( + f"Video codec: {camera_encoder.vcodec}, pixel format: {camera_encoder.pix_fmt}, " + f"GOP: {camera_encoder.g}, CRF: {camera_encoder.crf}" + ) # Create new features dict, converting image features to video features new_features = {} @@ -1769,11 +1771,7 @@ def convert_image_to_video_dataset( episode_indices=episode_indices, temp_dir=temp_dir, fps=fps, - vcodec=vcodec, - pix_fmt=pix_fmt, - g=g, - crf=crf, - fast_decode=fast_decode, + camera_encoder=camera_encoder, ) logging.info(f"Processing camera: {img_key}") @@ -1815,11 +1813,7 @@ def convert_image_to_video_dataset( imgs_dir=imgs_dir, video_path=video_path, fps=fps, - vcodec=vcodec, - pix_fmt=pix_fmt, - g=g, - crf=crf, - fast_decode=fast_decode, + camera_encoder=camera_encoder, overwrite=True, ) @@ -1865,7 +1859,9 @@ def convert_image_to_video_dataset( video_path = new_meta.root / new_meta.video_path.format( video_key=img_key, chunk_index=0, file_index=0 ) - new_meta.info.features[img_key]["info"] = get_video_info(video_path) + new_meta.info.features[img_key]["info"] = get_video_info( + video_path, camera_encoder=camera_encoder + ) write_info(new_meta.info, new_meta.root) diff --git a/src/lerobot/datasets/dataset_writer.py b/src/lerobot/datasets/dataset_writer.py index cf306a86a..6be63194f 100644 --- a/src/lerobot/datasets/dataset_writer.py +++ b/src/lerobot/datasets/dataset_writer.py @@ -31,6 +31,8 @@ import PIL.Image import pyarrow.parquet as pq import torch +from lerobot.configs import VideoEncoderConfig, camera_encoder_defaults + from .compute_stats import compute_episode_stats from .dataset_metadata import LeRobotDatasetMetadata from .feature_utils import ( @@ -65,14 +67,19 @@ def _encode_video_worker( episode_index: int, root: Path, fps: int, - vcodec: str = "libsvtav1", + camera_encoder: VideoEncoderConfig | None = None, encoder_threads: int | None = None, ) -> Path: temp_path = Path(tempfile.mkdtemp(dir=root)) / f"{video_key}_{episode_index:03d}.mp4" fpath = DEFAULT_IMAGE_PATH.format(image_key=video_key, episode_index=episode_index, frame_index=0) img_dir = (root / fpath).parent encode_video_frames( - img_dir, temp_path, fps, vcodec=vcodec, overwrite=True, encoder_threads=encoder_threads + img_dir, + temp_path, + fps, + camera_encoder=camera_encoder, + encoder_threads=encoder_threads, + overwrite=True, ) shutil.rmtree(img_dir) return temp_path @@ -89,20 +96,22 @@ class DatasetWriter: self, meta: LeRobotDatasetMetadata, root: Path, - vcodec: str, + camera_encoder: VideoEncoderConfig | None, encoder_threads: int | None, batch_encoding_size: int, streaming_encoder: StreamingVideoEncoder | None = None, initial_frames: int = 0, ): - """Initialize the writer with metadata, codec, and encoding config. + """Initialize the writer with metadata, codec, and encoder config. Args: meta: Dataset metadata instance (used for feature schema, chunk settings, and episode persistence). root: Local dataset root directory. - vcodec: Video codec for encoding (e.g. ``'libsvtav1'``, ``'h264'``). - encoder_threads: Threads per encoder instance. ``None`` for auto. + camera_encoder: Video encoder settings applied to all cameras. + ``None`` uses :func:`~lerobot.configs.camera_encoder_defaults`. + encoder_threads: Number of encoder threads (global). ``None`` + lets the codec decide. batch_encoding_size: Number of episodes to accumulate before batch-encoding videos. streaming_encoder: Optional pre-built :class:`StreamingVideoEncoder` @@ -111,7 +120,7 @@ class DatasetWriter: """ self._meta = meta self._root = root - self._vcodec = vcodec + self._camera_encoder = camera_encoder or camera_encoder_defaults() self._encoder_threads = encoder_threads self._batch_encoding_size = batch_encoding_size self._streaming_encoder = streaming_encoder @@ -284,7 +293,7 @@ class DatasetWriter: episode_index, self._root, self._meta.fps, - self._vcodec, + self._camera_encoder, self._encoder_threads, ): video_key for video_key in self._meta.video_keys @@ -495,7 +504,7 @@ class DatasetWriter: # Update video info (only needed when first episode is encoded) if episode_index == 0: - self._meta.update_video_info(video_key) + self._meta.update_video_info(video_key, camera_encoder=self._camera_encoder) write_info(self._meta.info, self._meta.root) metadata = { @@ -564,7 +573,12 @@ class DatasetWriter: def _encode_temporary_episode_video(self, video_key: str, episode_index: int) -> Path: """Use ffmpeg to convert frames stored as png into mp4 videos.""" return _encode_video_worker( - video_key, episode_index, self._root, self._meta.fps, self._vcodec, self._encoder_threads + video_key, + episode_index, + self._root, + self._meta.fps, + self._camera_encoder, + self._encoder_threads, ) def close_writer(self) -> None: diff --git a/src/lerobot/datasets/feature_utils.py b/src/lerobot/datasets/feature_utils.py index 2ab4b0ea6..d5a550a4c 100644 --- a/src/lerobot/datasets/feature_utils.py +++ b/src/lerobot/datasets/feature_utils.py @@ -19,6 +19,7 @@ import datasets import numpy as np from PIL import Image as PILImage +from lerobot.configs import VIDEO_ENCODER_INFO_KEYS from lerobot.utils.constants import DEFAULT_FEATURES from lerobot.utils.utils import is_valid_numpy_dtype_string @@ -108,6 +109,41 @@ def create_empty_dataset_info( ) +def features_equal_for_merge(features_a: dict[str, dict], features_b: dict[str, dict]) -> bool: + """Return whether two LeRobotDatasetMetadata ``features`` dicts are compatible for aggregation. + + For video features, keys under ``info`` related to video encoding parameters are ignored during + comparison as they do not prevent aggregation. + """ + + def _without_encoder_info_keys(feature: dict) -> dict: + filtered = dict(feature) + filtered_info = filtered.get("info") + if isinstance(filtered_info, dict): + filtered["info"] = { + info_key: info_value + for info_key, info_value in filtered_info.items() + if info_key not in VIDEO_ENCODER_INFO_KEYS + } + return filtered + + if set(features_a) != set(features_b): + return False + for key in features_a: + fa_key = features_a[key] + fb_key = features_b[key] + if fa_key.get("dtype") != fb_key.get("dtype"): + return False + if fa_key.get("dtype") != "video": + if fa_key != fb_key: + return False + continue + + if _without_encoder_info_keys(fa_key) != _without_encoder_info_keys(fb_key): + return False + return True + + def check_delta_timestamps( delta_timestamps: dict[str, list[float]], fps: int, tolerance_s: float, raise_value_error: bool = True ) -> bool: diff --git a/src/lerobot/datasets/lerobot_dataset.py b/src/lerobot/datasets/lerobot_dataset.py index ab55aa9f8..9734bcc74 100644 --- a/src/lerobot/datasets/lerobot_dataset.py +++ b/src/lerobot/datasets/lerobot_dataset.py @@ -24,6 +24,7 @@ import torch.utils from huggingface_hub import HfApi, snapshot_download from huggingface_hub.errors import RevisionNotFoundError +from lerobot.configs import VideoEncoderConfig from lerobot.utils.constants import HF_LEROBOT_HUB_CACHE from .dataset_metadata import CODEBASE_VERSION, LeRobotDatasetMetadata @@ -36,8 +37,7 @@ from .utils import ( ) from .video_utils import ( StreamingVideoEncoder, - get_safe_default_codec, - resolve_vcodec, + get_safe_default_video_backend, ) logger = logging.getLogger(__name__) @@ -59,10 +59,10 @@ class LeRobotDataset(torch.utils.data.Dataset): video_backend: str | None = None, return_uint8: bool = False, batch_encoding_size: int = 1, - vcodec: str = "libsvtav1", + camera_encoder: VideoEncoderConfig | None = None, + encoder_threads: int | None = None, streaming_encoding: bool = False, encoder_queue_maxsize: int = 30, - encoder_threads: int | None = None, ): """ 2 modes are available for instantiating this class, depending on 2 different use cases: @@ -183,16 +183,15 @@ class LeRobotDataset(torch.utils.data.Dataset): You can also use the 'pyav' decoder used by Torchvision, which used to be the default option, or 'video_reader' which is another decoder of Torchvision. batch_encoding_size (int, optional): Number of episodes to accumulate before batch encoding videos. Set to 1 for immediate encoding (default), or higher for batched encoding. Defaults to 1. - vcodec (str, optional): Video codec for encoding videos during recording. Options: 'h264', 'hevc', - 'libsvtav1', 'auto', or hardware-specific codecs like 'h264_videotoolbox', 'h264_nvenc'. - Defaults to 'libsvtav1'. Use 'auto' to auto-detect the best available hardware encoder. + camera_encoder (VideoEncoderConfig | None, optional): Video encoder settings for cameras + (codec, quality, etc.). When ``None``, :func:`~lerobot.configs.video.camera_encoder_defaults` + is used by the writer. + encoder_threads (int | None, optional): Number of encoder threads (global). ``None`` lets the + codec decide. streaming_encoding (bool, optional): If True, encode video frames in real-time during capture instead of writing PNG images first. This makes save_episode() near-instant. Defaults to False. encoder_queue_maxsize (int, optional): Maximum number of frames to buffer per camera when using streaming encoding. Defaults to 30 (~1s at 30fps). - encoder_threads (int | None, optional): Number of threads per encoder instance. None lets the - codec auto-detect (default). Lower values reduce CPU usage per encoder. Maps to 'lp' (via svtav1-params) for - libsvtav1 and 'threads' for h264/hevc. Note: Write-mode parameters (``streaming_encoding``, ``batch_encoding_size``) passed to @@ -207,10 +206,9 @@ class LeRobotDataset(torch.utils.data.Dataset): self.delta_timestamps = delta_timestamps self.tolerance_s = tolerance_s self.revision = revision if revision else CODEBASE_VERSION - self._video_backend = video_backend if video_backend else get_safe_default_codec() + self._video_backend = video_backend if video_backend else get_safe_default_video_backend() self._return_uint8 = return_uint8 self._batch_encoding_size = batch_encoding_size - self._vcodec = resolve_vcodec(vcodec) self._encoder_threads = encoder_threads if self._requested_root is not None: @@ -273,12 +271,15 @@ class LeRobotDataset(torch.utils.data.Dataset): streaming_enc = None if streaming_encoding and len(self.meta.video_keys) > 0: streaming_enc = self._build_streaming_encoder( - self.meta.fps, self._vcodec, encoder_queue_maxsize, encoder_threads + self.meta.fps, + camera_encoder, + encoder_queue_maxsize, + encoder_threads, ) self.writer = DatasetWriter( meta=self.meta, root=self.root, - vcodec=self._vcodec, + camera_encoder=camera_encoder, encoder_threads=encoder_threads, batch_encoding_size=batch_encoding_size, streaming_encoder=streaming_enc, @@ -320,17 +321,13 @@ class LeRobotDataset(torch.utils.data.Dataset): @staticmethod def _build_streaming_encoder( fps: int, - vcodec: str, + camera_encoder: VideoEncoderConfig | None, encoder_queue_maxsize: int, encoder_threads: int | None, ) -> StreamingVideoEncoder: return StreamingVideoEncoder( fps=fps, - vcodec=vcodec, - pix_fmt="yuv420p", - g=2, - crf=30, - preset=None, + camera_encoder=camera_encoder, queue_maxsize=encoder_queue_maxsize, encoder_threads=encoder_threads, ) @@ -647,7 +644,7 @@ class LeRobotDataset(torch.utils.data.Dataset): image_writer_threads: int = 0, video_backend: str | None = None, batch_encoding_size: int = 1, - vcodec: str = "libsvtav1", + camera_encoder: VideoEncoderConfig | None = None, metadata_buffer_size: int = 10, streaming_encoding: bool = False, encoder_queue_maxsize: int = 30, @@ -678,20 +675,20 @@ class LeRobotDataset(torch.utils.data.Dataset): video_backend: Video decoding backend (used when reading back). batch_encoding_size: Number of episodes to accumulate before batch-encoding videos. ``1`` means encode immediately. - vcodec: Video codec for encoding. Options include ``'libsvtav1'``, - ``'h264'``, ``'hevc'``, ``'auto'``. + camera_encoder: Video encoder settings for cameras (codec, quality, etc.). + When ``None``, :func:`~lerobot.configs.video.camera_encoder_defaults` is used. + encoder_threads: Number of encoder threads (global). ``None`` + lets the codec decide. metadata_buffer_size: Number of episode metadata records to buffer before flushing to parquet. streaming_encoding: If ``True``, encode video frames in real-time during capture instead of writing images first. encoder_queue_maxsize: Max buffered frames per camera when using streaming encoding. - encoder_threads: Threads per encoder instance. ``None`` for auto. Returns: A new :class:`LeRobotDataset` in write mode. """ - vcodec = resolve_vcodec(vcodec) obj = cls.__new__(cls) obj.meta = LeRobotDatasetMetadata.create( repo_id=repo_id, @@ -712,23 +709,23 @@ class LeRobotDataset(torch.utils.data.Dataset): obj.image_transforms = None obj.delta_timestamps = None obj.episodes = None - obj._video_backend = video_backend if video_backend is not None else get_safe_default_codec() + obj._video_backend = video_backend if video_backend is not None else get_safe_default_video_backend() obj._return_uint8 = False obj._batch_encoding_size = batch_encoding_size - obj._vcodec = vcodec obj._encoder_threads = encoder_threads # Reader is lazily created on first access (write-only mode) obj.reader = None - # Create writer streaming_enc = None if streaming_encoding and len(obj.meta.video_keys) > 0: - streaming_enc = cls._build_streaming_encoder(fps, vcodec, encoder_queue_maxsize, encoder_threads) + streaming_enc = cls._build_streaming_encoder( + fps, camera_encoder, encoder_queue_maxsize, encoder_threads + ) obj.writer = DatasetWriter( meta=obj.meta, root=obj.root, - vcodec=vcodec, + camera_encoder=camera_encoder, encoder_threads=encoder_threads, batch_encoding_size=batch_encoding_size, streaming_encoder=streaming_enc, @@ -751,12 +748,12 @@ class LeRobotDataset(torch.utils.data.Dataset): force_cache_sync: bool = False, video_backend: str | None = None, batch_encoding_size: int = 1, - vcodec: str = "libsvtav1", + camera_encoder: VideoEncoderConfig | None = None, + encoder_threads: int | None = None, image_writer_processes: int = 0, image_writer_threads: int = 0, streaming_encoding: bool = False, encoder_queue_maxsize: int = 30, - encoder_threads: int | None = None, ) -> "LeRobotDataset": """Resume recording on an existing dataset. @@ -779,13 +776,15 @@ class LeRobotDataset(torch.utils.data.Dataset): video_backend: Video decoding backend for reading back data. batch_encoding_size: Number of episodes to accumulate before batch-encoding videos. - vcodec: Video codec for encoding. + camera_encoder: Video encoder settings for cameras (codec, quality, etc.). + When ``None``, :func:`~lerobot.configs.video.camera_encoder_defaults` is used. + encoder_threads: Number of encoder threads (global). ``None`` + lets the codec decide. image_writer_processes: Subprocesses for async image writing. image_writer_threads: Threads for async image writing. streaming_encoding: If ``True``, encode video in real-time during capture. encoder_queue_maxsize: Max buffered frames per camera for streaming. - encoder_threads: Threads per encoder instance. ``None`` for auto. Returns: A :class:`LeRobotDataset` in write mode, ready to append episodes. @@ -796,7 +795,6 @@ class LeRobotDataset(torch.utils.data.Dataset): "Writing into the revision-safe Hub snapshot cache (used when root=None) would corrupt " "the shared cache. Please provide a local directory path." ) - vcodec = resolve_vcodec(vcodec) obj = cls.__new__(cls) obj.repo_id = repo_id obj._requested_root = Path(root) @@ -805,11 +803,9 @@ class LeRobotDataset(torch.utils.data.Dataset): obj.image_transforms = None obj.delta_timestamps = None obj.episodes = None - obj._video_backend = video_backend if video_backend else get_safe_default_codec() + obj._video_backend = video_backend if video_backend else get_safe_default_video_backend() obj._return_uint8 = False obj._batch_encoding_size = batch_encoding_size - obj._vcodec = vcodec - obj._encoder_threads = encoder_threads if obj._requested_root is not None: obj._requested_root.mkdir(exist_ok=True, parents=True) @@ -818,21 +814,22 @@ class LeRobotDataset(torch.utils.data.Dataset): obj.meta = LeRobotDatasetMetadata( obj.repo_id, obj._requested_root, obj.revision, force_cache_sync=force_cache_sync ) + + obj._encoder_threads = encoder_threads obj.root = obj.meta.root # Reader is lazily created on first access (write-only mode) obj.reader = None - # Create writer for appending streaming_enc = None if streaming_encoding and len(obj.meta.video_keys) > 0: streaming_enc = cls._build_streaming_encoder( - obj.meta.fps, vcodec, encoder_queue_maxsize, encoder_threads + obj.meta.fps, camera_encoder, encoder_queue_maxsize, encoder_threads ) obj.writer = DatasetWriter( meta=obj.meta, root=obj.root, - vcodec=vcodec, + camera_encoder=camera_encoder, encoder_threads=encoder_threads, batch_encoding_size=batch_encoding_size, streaming_encoder=streaming_enc, diff --git a/src/lerobot/datasets/pyav_utils.py b/src/lerobot/datasets/pyav_utils.py new file mode 100644 index 000000000..d291f8b40 --- /dev/null +++ b/src/lerobot/datasets/pyav_utils.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python + +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""PyAV-based compatibility checks for :class:`VideoEncoderConfig`. + +Centralises all :mod:`av` introspection of the bundled FFmpeg build. +Checks degrade to a no-op when the target codec isn't available locally. +""" + +import functools +import logging +from typing import Any + +import av + +logger = logging.getLogger(__name__) + +FFMPEG_NUMERIC_OPTION_TYPES = ("INT", "INT64", "UINT64", "FLOAT", "DOUBLE") +FFMPEG_INTEGER_OPTION_TYPES = ("INT", "INT64", "UINT64") + + +@functools.cache +def get_codec(vcodec: str) -> av.codec.Codec | None: + """PyAV write-mode ``Codec`` for *vcodec*, or ``None`` if unavailable.""" + try: + return av.codec.Codec(vcodec, "w") + except Exception: + return None + + +@functools.cache +def _get_codec_options_by_name(vcodec: str) -> dict[str, av.option.Option]: + """Private-option name → PyAV ``Option`` for *vcodec* (empty if unavailable).""" + codec = get_codec(vcodec) + if codec is None: + return {} + return {opt.name: opt for opt in codec.descriptor.options} + + +@functools.cache +def _get_codec_video_formats(vcodec: str) -> tuple[str, ...]: + """Pixel formats accepted by *vcodec* in PyAV's preferred order (empty if unknown).""" + codec = get_codec(vcodec) + if codec is None: + return () + return tuple(fmt.name for fmt in (codec.video_formats or [])) + + +def detect_available_encoders_pyav(encoders: list[str] | str) -> list[str]: + """Return the subset of *encoders* available as video encoders in the local FFmpeg build. + + Each name is probed directly via :func:`get_codec`; input order is preserved. + """ + if isinstance(encoders, str): + encoders = [encoders] + + available: list[str] = [] + for name in encoders: + codec = get_codec(name) + if codec is not None and codec.type == "video": + available.append(name) + else: + logger.debug("encoder '%s' not available as video encoder", name) + return available + + +def _check_option_value(vcodec: str, label: str, value: Any, opt: av.option.Option) -> None: + """Range-check numeric *value* and choice-check string *value* against *opt*.""" + type_name = opt.type.name + if type_name in FFMPEG_NUMERIC_OPTION_TYPES: + if isinstance(value, bool): + raise ValueError( + f"{label}={value!r} is not numeric; codec {vcodec!r} expects a number for this option." + ) + elif isinstance(value, str): + try: + num_val = float(value) + except ValueError as e: + raise ValueError( + f"{label}={value!r} is not numeric; codec {vcodec!r} expects a number for this option." + ) from e + elif isinstance(value, (float, int)): + num_val = value + else: + raise ValueError( + f"{label}={value!r} is not numeric; codec {vcodec!r} expects a number for this option." + ) + + # Check integer type compatibility + if type_name in FFMPEG_INTEGER_OPTION_TYPES and not num_val.is_integer(): + raise ValueError( + f"{label}={num_val!r} must be an integer for codec {vcodec!r} " + f"(FFmpeg option {opt.name!r} is {type_name}); float values are not allowed." + ) + + # Check numeric range compatibility + lo, hi = float(opt.min), float(opt.max) + if lo < hi and not (lo <= num_val <= hi): + raise ValueError( + f"{label}={num_val} is out of range for codec {vcodec!r}; must be in [{lo}, {hi}]" + ) + + elif type_name == "STRING": + if isinstance(value, bool): + raise ValueError(f"{label}={value!r} is not a valid string value for codec {vcodec!r}.") + if isinstance(value, str): + str_val = value + elif isinstance(value, (int, float)): + str_val = str(value) + else: + raise ValueError(f"{label}={value!r} has unsupported type for STRING option on codec {vcodec!r}") + + # Check string choice compatibility + choices = [c.name for c in (opt.choices or [])] + if choices and str_val not in choices: + raise ValueError( + f"{label}={str_val!r} is not a supported choice for codec " + f"{vcodec!r}; valid choices: {choices}" + ) + else: + return + + +def _check_pixel_format(vcodec: str, pix_fmt: str) -> None: + formats = _get_codec_video_formats(vcodec) + if formats and pix_fmt not in formats: + raise ValueError( + f"pix_fmt={pix_fmt!r} is not supported by codec {vcodec!r}; " + f"supported pixel formats: {list(formats)}" + ) + + +def _check_codec_options(vcodec: str, codec_options: dict[str, Any]) -> None: + """Validate merged encoder options (typed) against the codec's published AVOptions.""" + supported_options = _get_codec_options_by_name(vcodec) + for key, value in codec_options.items(): + # GOP size is not a codec-specific option, it has to be validated separately. + if key == "g": + if isinstance(value, bool) or not isinstance(value, int) or value < 1: + raise ValueError(f"g={value!r} must be a positive integer for codec {vcodec!r}") + continue + if key not in supported_options: + continue + _check_option_value(vcodec, key, value, supported_options[key]) + + +def check_video_encoder_parameters_pyav(vcodec: str, pix_fmt: str, codec_options: dict[str, Any]) -> None: + """Verify *config* is compatible with the bundled FFmpeg build. + + Checks pixel format, abstract tuning-field compatibility, and each merged + encoder option from :meth:`~lerobot.configs.video.VideoEncoderConfig.get_codec_options` + against PyAV (including numeric ``extra_options`` present in that dict). + No-op when ``config.vcodec`` isn't in the local FFmpeg build. + + Raises: + ValueError: on the first incompatibility encountered. + """ + options = _get_codec_options_by_name(vcodec) + if not options: + raise ValueError(f"Codec {vcodec!r} is not available in the bundled FFmpeg build") + _check_pixel_format(vcodec, pix_fmt) + _check_codec_options(vcodec, codec_options) diff --git a/src/lerobot/datasets/video_utils.py b/src/lerobot/datasets/video_utils.py index 00ff09ee7..e823a406c 100644 --- a/src/lerobot/datasets/video_utils.py +++ b/src/lerobot/datasets/video_utils.py @@ -22,7 +22,7 @@ import shutil import tempfile import threading import warnings -from dataclasses import dataclass, field +from dataclasses import asdict, dataclass, field from fractions import Fraction from pathlib import Path from threading import Lock @@ -36,86 +36,14 @@ import torch from datasets.features.features import register_feature from PIL import Image -from lerobot.utils.import_utils import get_safe_default_codec +from lerobot.configs import ( + VideoEncoderConfig, + camera_encoder_defaults, +) +from lerobot.utils.import_utils import get_safe_default_video_backend logger = logging.getLogger(__name__) -# List of hardware encoders to probe for auto-selection. Availability depends on the platform and FFmpeg build. -# Determines the order of preference for auto-selection when vcodec="auto" is used. -HW_ENCODERS = [ - "h264_videotoolbox", # macOS - "hevc_videotoolbox", # macOS - "h264_nvenc", # NVIDIA GPU - "hevc_nvenc", # NVIDIA GPU - "h264_vaapi", # Linux Intel/AMD - "h264_qsv", # Intel Quick Sync -] - -VALID_VIDEO_CODECS = {"h264", "hevc", "libsvtav1", "auto"} | set(HW_ENCODERS) - - -def _get_codec_options( - vcodec: str, - g: int | None = 2, - crf: int | None = 30, - preset: int | None = None, -) -> dict: - """Build codec-specific options dict for video encoding.""" - options = {} - - # GOP size (keyframe interval) - supported by VideoToolbox and software encoders - if g is not None and (vcodec in ("h264_videotoolbox", "hevc_videotoolbox") or vcodec not in HW_ENCODERS): - options["g"] = str(g) - - # Quality control (codec-specific parameter names) - if crf is not None: - if vcodec in ("h264", "hevc", "libsvtav1"): - options["crf"] = str(crf) - elif vcodec in ("h264_videotoolbox", "hevc_videotoolbox"): - quality = max(1, min(100, int(100 - crf * 2))) - options["q:v"] = str(quality) - elif vcodec in ("h264_nvenc", "hevc_nvenc"): - options["rc"] = "constqp" - options["qp"] = str(crf) - elif vcodec in ("h264_vaapi",): - options["qp"] = str(crf) - elif vcodec in ("h264_qsv",): - options["global_quality"] = str(crf) - - # Preset (only for libsvtav1) - if vcodec == "libsvtav1": - options["preset"] = str(preset) if preset is not None else "12" - - return options - - -def detect_available_hw_encoders() -> list[str]: - """Probe PyAV/FFmpeg for available hardware video encoders.""" - available = [] - for codec_name in HW_ENCODERS: - try: - av.codec.Codec(codec_name, "w") - available.append(codec_name) - except Exception: # nosec B110 - logger.debug("HW encoder '%s' not available", codec_name) # nosec B110 - return available - - -def resolve_vcodec(vcodec: str) -> str: - """Validate vcodec and resolve 'auto' to best available HW encoder, fallback to libsvtav1.""" - if vcodec not in VALID_VIDEO_CODECS: - raise ValueError(f"Invalid vcodec '{vcodec}'. Must be one of: {sorted(VALID_VIDEO_CODECS)}") - if vcodec != "auto": - logger.info(f"Using video codec: {vcodec}") - return vcodec - available = detect_available_hw_encoders() - for encoder in HW_ENCODERS: - if encoder in available: - logger.info(f"Auto-selected video codec: {encoder}") - return encoder - logger.info("No hardware encoder available, falling back to software encoder 'libsvtav1'") - return "libsvtav1" - def decode_video_frames( video_path: Path | str, @@ -143,7 +71,7 @@ def decode_video_frames( Currently supports torchcodec on cpu and pyav. """ if backend is None: - backend = get_safe_default_codec() + backend = get_safe_default_video_backend() if backend == "torchcodec": return decode_video_frames_torchcodec(video_path, timestamps, tolerance_s, return_uint8=return_uint8) elif backend == "pyav": @@ -407,18 +335,17 @@ def encode_video_frames( imgs_dir: Path | str, video_path: Path | str, fps: int, - vcodec: str = "libsvtav1", - pix_fmt: str = "yuv420p", - g: int | None = 2, - crf: int | None = 30, - fast_decode: int = 0, + camera_encoder: VideoEncoderConfig | None = None, + encoder_threads: int | None = None, + *, log_level: int | None = av.logging.WARNING, overwrite: bool = False, - preset: int | None = None, - encoder_threads: int | None = None, ) -> None: """More info on ffmpeg arguments tuning on `benchmark/video/README.md`""" - vcodec = resolve_vcodec(vcodec) + if camera_encoder is None: + camera_encoder = camera_encoder_defaults() + vcodec = camera_encoder.vcodec + pix_fmt = camera_encoder.pix_fmt video_path = Path(video_path) imgs_dir = Path(imgs_dir) @@ -429,42 +356,18 @@ def encode_video_frames( video_path.parent.mkdir(parents=True, exist_ok=True) - # Encoders/pixel formats incompatibility check - if (vcodec == "libsvtav1" or vcodec == "hevc") and pix_fmt == "yuv444p": - logger.warning( - f"Incompatible pixel format 'yuv444p' for codec {vcodec}, auto-selecting format 'yuv420p'" - ) - pix_fmt = "yuv420p" - # Get input frames template = "frame-" + ("[0-9]" * 6) + ".png" input_list = sorted( glob.glob(str(imgs_dir / template)), key=lambda x: int(x.split("-")[-1].split(".")[0]) ) - # Define video output frame size (assuming all input frames are the same size) if len(input_list) == 0: raise FileNotFoundError(f"No images found in {imgs_dir}.") with Image.open(input_list[0]) as dummy_image: width, height = dummy_image.size - # Define video codec options - video_options = _get_codec_options(vcodec, g, crf, preset) - - if fast_decode: - key = "svtav1-params" if vcodec == "libsvtav1" else "tune" - value = f"fast-decode={fast_decode}" if vcodec == "libsvtav1" else "fastdecode" - video_options[key] = value - - if encoder_threads is not None: - if vcodec == "libsvtav1": - lp_param = f"lp={encoder_threads}" - if "svtav1-params" in video_options: - video_options["svtav1-params"] += f":{lp_param}" - else: - video_options["svtav1-params"] = lp_param - else: - video_options["threads"] = str(encoder_threads) + video_options = camera_encoder.get_codec_options(encoder_threads, as_strings=True) # Set logging level if log_level is not None: @@ -501,7 +404,10 @@ def encode_video_frames( def concatenate_video_files( - input_video_paths: list[Path | str], output_video_path: Path, overwrite: bool = True + input_video_paths: list[Path | str], + output_video_path: Path, + overwrite: bool = True, + compatibility_check: bool = False, ): """ Concatenate multiple video files into a single video file using pyav. @@ -514,6 +420,7 @@ def concatenate_video_files( input_video_paths: Ordered list of input video file paths to concatenate. output_video_path: Path to the output video file. overwrite: Whether to overwrite the output video file if it already exists. Default is True. + compatibility_check: Whether to check if the input videos are compatible. Default is False. Note: - Creates a temporary directory for intermediate files that is cleaned up after use. @@ -532,6 +439,22 @@ def concatenate_video_files( if len(input_video_paths) == 0: raise FileNotFoundError("No input video paths provided.") + # This check may be skipped at recording time as videos are encoded with the same encoder config. + if compatibility_check: + reference_video_info = get_video_info(input_video_paths[0]) + for input_path in input_video_paths[1:]: + video_info = get_video_info(input_path) + if ( + video_info["video.height"] != reference_video_info["video.height"] + or video_info["video.width"] != reference_video_info["video.width"] + or video_info["video.fps"] != reference_video_info["video.fps"] + or video_info["video.codec"] != reference_video_info["video.codec"] + or video_info["video.pix_fmt"] != reference_video_info["video.pix_fmt"] + ): + raise ValueError( + f"Input video {input_path} is not compatible with the reference video {input_video_paths[0]}." + ) + # Create a temporary .ffconcat file to list the input video paths with tempfile.NamedTemporaryFile(mode="w", suffix=".ffconcat", delete=False) as tmp_concatenate_file: tmp_concatenate_file.write("ffconcat version 1.0\n") @@ -598,26 +521,20 @@ class _CameraEncoderThread(threading.Thread): fps: int, vcodec: str, pix_fmt: str, - g: int | None, - crf: int | None, - preset: int | None, + codec_options: dict[str, str], frame_queue: queue.Queue, result_queue: queue.Queue, stop_event: threading.Event, - encoder_threads: int | None = None, ): super().__init__(daemon=True) self.video_path = video_path self.fps = fps self.vcodec = vcodec self.pix_fmt = pix_fmt - self.g = g - self.crf = crf - self.preset = preset + self.codec_options = codec_options self.frame_queue = frame_queue self.result_queue = result_queue self.stop_event = stop_event - self.encoder_threads = encoder_threads def run(self) -> None: from .compute_stats import RunningQuantileStats, auto_downsample_height_width @@ -653,19 +570,9 @@ class _CameraEncoderThread(threading.Thread): # Open container on first frame (to get width/height) if container is None: height, width = frame_data.shape[:2] - video_options = _get_codec_options(self.vcodec, self.g, self.crf, self.preset) - if self.encoder_threads is not None: - if self.vcodec == "libsvtav1": - lp_param = f"lp={self.encoder_threads}" - if "svtav1-params" in video_options: - video_options["svtav1-params"] += f":{lp_param}" - else: - video_options["svtav1-params"] = lp_param - else: - video_options["threads"] = str(self.encoder_threads) Path(self.video_path).parent.mkdir(parents=True, exist_ok=True) container = av.open(str(self.video_path), "w") - output_stream = container.add_stream(self.vcodec, self.fps, options=video_options) + output_stream = container.add_stream(self.vcodec, self.fps, options=self.codec_options) output_stream.pix_fmt = self.pix_fmt output_stream.width = width output_stream.height = height @@ -731,22 +638,24 @@ class StreamingVideoEncoder: def __init__( self, fps: int, - vcodec: str = "libsvtav1", - pix_fmt: str = "yuv420p", - g: int | None = 2, - crf: int | None = 30, - preset: int | None = None, + camera_encoder: VideoEncoderConfig | None = None, queue_maxsize: int = 30, encoder_threads: int | None = None, ): + """ + Args: + fps: Frames per second for the output videos. + camera_encoder: Video encoder settings applied to all cameras. + When ``None``, :func:`camera_encoder_defaults` is used. + encoder_threads: Number of encoder threads (global setting). + ``None`` lets the codec decide. + queue_maxsize: Max frames to buffer per camera before + back-pressure drops frames. + """ self.fps = fps - self.vcodec = resolve_vcodec(vcodec) - self.pix_fmt = pix_fmt - self.g = g - self.crf = crf - self.preset = preset + self._camera_encoder = camera_encoder or camera_encoder_defaults() + self._encoder_threads = encoder_threads self.queue_maxsize = queue_maxsize - self.encoder_threads = encoder_threads self._frame_queues: dict[str, queue.Queue] = {} self._result_queues: dict[str, queue.Queue] = {} @@ -777,18 +686,17 @@ class StreamingVideoEncoder: temp_video_dir = Path(tempfile.mkdtemp(dir=temp_dir)) video_path = temp_video_dir / f"{video_key.replace('/', '_')}_streaming.mp4" + vcodec = self._camera_encoder.vcodec + codec_options = self._camera_encoder.get_codec_options(self._encoder_threads, as_strings=True) encoder_thread = _CameraEncoderThread( video_path=video_path, fps=self.fps, - vcodec=self.vcodec, - pix_fmt=self.pix_fmt, - g=self.g, - crf=self.crf, - preset=self.preset, + vcodec=vcodec, + pix_fmt=self._camera_encoder.pix_fmt, + codec_options=codec_options, frame_queue=frame_queue, result_queue=result_queue, stop_event=stop_event, - encoder_threads=self.encoder_threads, ) encoder_thread.start() @@ -993,8 +901,18 @@ def get_audio_info(video_path: Path | str) -> dict: return audio_info -def get_video_info(video_path: Path | str) -> dict: - # Set logging level +def get_video_info( + video_path: Path | str, + camera_encoder: VideoEncoderConfig | None = None, +) -> dict: + """Build the ``video.*`` / ``audio.*`` info dict persisted in ``info.json``. + + Args: + video_path: Path to the encoded video file to probe. + camera_encoder: If provided, record the exact encoder settings used to encode this + video. Stream-derived values take precedence — encoder fields are only written for keys + not already populated from the video file itself. + """ logging.getLogger("libav").setLevel(av.logging.WARNING) # Getting video stream information @@ -1025,6 +943,14 @@ def get_video_info(video_path: Path | str) -> dict: # Adding audio stream information video_info.update(**get_audio_info(video_path)) + # Add additional encoder configuration if provided + if camera_encoder is not None: + for field_name, field_value in asdict(camera_encoder).items(): + # vcodec is already populated from the video stream + if field_name == "vcodec": + continue + video_info.setdefault(f"video.{field_name}", field_value) + return video_info diff --git a/src/lerobot/policies/eo1/modeling_eo1.py b/src/lerobot/policies/eo1/modeling_eo1.py index 27d609ec1..1c5860de5 100644 --- a/src/lerobot/policies/eo1/modeling_eo1.py +++ b/src/lerobot/policies/eo1/modeling_eo1.py @@ -28,11 +28,12 @@ import torch.nn.functional as F # noqa: N812 import torch.utils.checkpoint from torch import Tensor -from lerobot.policies.eo1.configuration_eo1 import EO1Config -from lerobot.policies.pretrained import PreTrainedPolicy from lerobot.utils.constants import ACTION, OBS_STATE from lerobot.utils.import_utils import _transformers_available, require_package +from ..pretrained import PreTrainedPolicy +from .configuration_eo1 import EO1Config + if TYPE_CHECKING or _transformers_available: from transformers.activations import ACT2FN from transformers.models.qwen2_5_vl import Qwen2_5_VLForConditionalGeneration diff --git a/src/lerobot/policies/eo1/processor_eo1.py b/src/lerobot/policies/eo1/processor_eo1.py index 2d7bb48ae..b1f32756a 100644 --- a/src/lerobot/policies/eo1/processor_eo1.py +++ b/src/lerobot/policies/eo1/processor_eo1.py @@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Any import torch from lerobot.configs.types import FeatureType, PipelineFeatureType, PolicyFeature -from lerobot.policies.eo1.configuration_eo1 import EO1Config from lerobot.processor import ( AddBatchDimensionProcessorStep, ComplementaryDataProcessorStep, @@ -44,6 +43,8 @@ from lerobot.utils.constants import ( ) from lerobot.utils.import_utils import _transformers_available, require_package +from .configuration_eo1 import EO1Config + if TYPE_CHECKING or _transformers_available: from transformers.models.qwen2_5_vl import Qwen2_5_VLProcessor else: diff --git a/src/lerobot/rewards/classifier/modeling_classifier.py b/src/lerobot/rewards/classifier/modeling_classifier.py index 1d8057135..ca02b532f 100644 --- a/src/lerobot/rewards/classifier/modeling_classifier.py +++ b/src/lerobot/rewards/classifier/modeling_classifier.py @@ -17,10 +17,11 @@ import logging import torch from torch import Tensor, nn -from lerobot.rewards.classifier.configuration_classifier import RewardClassifierConfig -from lerobot.rewards.pretrained import PreTrainedRewardModel from lerobot.utils.constants import OBS_IMAGE, REWARD +from ..pretrained import PreTrainedRewardModel +from .configuration_classifier import RewardClassifierConfig + class ClassifierOutput: """Wrapper for classifier outputs with additional metadata.""" diff --git a/src/lerobot/rewards/classifier/processor_classifier.py b/src/lerobot/rewards/classifier/processor_classifier.py index 056d7e91b..a5f609d0c 100644 --- a/src/lerobot/rewards/classifier/processor_classifier.py +++ b/src/lerobot/rewards/classifier/processor_classifier.py @@ -25,7 +25,8 @@ from lerobot.processor import ( policy_action_to_transition, transition_to_policy_action, ) -from lerobot.rewards.classifier.configuration_classifier import RewardClassifierConfig + +from .configuration_classifier import RewardClassifierConfig def make_classifier_processor( diff --git a/src/lerobot/rewards/factory.py b/src/lerobot/rewards/factory.py index f6716f3fb..c173f44a5 100644 --- a/src/lerobot/rewards/factory.py +++ b/src/lerobot/rewards/factory.py @@ -22,9 +22,10 @@ import torch from lerobot.configs.rewards import RewardModelConfig from lerobot.processor import PolicyAction, PolicyProcessorPipeline -from lerobot.rewards.classifier.configuration_classifier import RewardClassifierConfig -from lerobot.rewards.pretrained import PreTrainedRewardModel -from lerobot.rewards.sarm.configuration_sarm import SARMConfig + +from .classifier.configuration_classifier import RewardClassifierConfig +from .pretrained import PreTrainedRewardModel +from .sarm.configuration_sarm import SARMConfig def get_reward_model_class(name: str) -> type[PreTrainedRewardModel]: diff --git a/src/lerobot/rewards/sarm/compute_rabc_weights.py b/src/lerobot/rewards/sarm/compute_rabc_weights.py index b1bf2e1f5..bdbb0d297 100644 --- a/src/lerobot/rewards/sarm/compute_rabc_weights.py +++ b/src/lerobot/rewards/sarm/compute_rabc_weights.py @@ -58,9 +58,10 @@ import torch from tqdm import tqdm from lerobot.datasets import LeRobotDataset -from lerobot.rewards.sarm.modeling_sarm import SARMRewardModel -from lerobot.rewards.sarm.processor_sarm import make_sarm_pre_post_processors -from lerobot.rewards.sarm.sarm_utils import normalize_stage_tau + +from .modeling_sarm import SARMRewardModel +from .processor_sarm import make_sarm_pre_post_processors +from .sarm_utils import normalize_stage_tau def get_reward_model_path_from_parquet(parquet_path: Path) -> str | None: diff --git a/src/lerobot/rewards/sarm/modeling_sarm.py b/src/lerobot/rewards/sarm/modeling_sarm.py index 365f519b2..5ebd42d30 100644 --- a/src/lerobot/rewards/sarm/modeling_sarm.py +++ b/src/lerobot/rewards/sarm/modeling_sarm.py @@ -32,13 +32,14 @@ import torch.nn as nn import torch.nn.functional as F # noqa: N812 from torch import Tensor -from lerobot.rewards.pretrained import PreTrainedRewardModel -from lerobot.rewards.sarm.configuration_sarm import SARMConfig -from lerobot.rewards.sarm.sarm_utils import ( +from lerobot.utils.constants import OBS_STR + +from ..pretrained import PreTrainedRewardModel +from .configuration_sarm import SARMConfig +from .sarm_utils import ( normalize_stage_tau, pad_state_to_max_dim, ) -from lerobot.utils.constants import OBS_STR class StageTransformer(nn.Module): diff --git a/src/lerobot/rewards/sarm/processor_sarm.py b/src/lerobot/rewards/sarm/processor_sarm.py index eaa5f66f5..37db374d4 100644 --- a/src/lerobot/rewards/sarm/processor_sarm.py +++ b/src/lerobot/rewards/sarm/processor_sarm.py @@ -58,15 +58,16 @@ from lerobot.processor import ( policy_action_to_transition, transition_to_policy_action, ) -from lerobot.rewards.sarm.configuration_sarm import SARMConfig -from lerobot.rewards.sarm.sarm_utils import ( +from lerobot.types import EnvTransition, PolicyAction, TransitionKey +from lerobot.utils.constants import POLICY_POSTPROCESSOR_DEFAULT_NAME, POLICY_PREPROCESSOR_DEFAULT_NAME + +from .configuration_sarm import SARMConfig +from .sarm_utils import ( apply_rewind_augmentation, compute_absolute_indices, find_stage_and_tau, pad_state_to_max_dim, ) -from lerobot.types import EnvTransition, PolicyAction, TransitionKey -from lerobot.utils.constants import POLICY_POSTPROCESSOR_DEFAULT_NAME, POLICY_PREPROCESSOR_DEFAULT_NAME class SARMEncodingProcessorStep(ProcessorStep): diff --git a/src/lerobot/rollout/context.py b/src/lerobot/rollout/context.py index 8804cd789..bf5fa0fd4 100644 --- a/src/lerobot/rollout/context.py +++ b/src/lerobot/rollout/context.py @@ -332,7 +332,7 @@ def build_rollout_context( cfg.dataset.repo_id, root=cfg.dataset.root, batch_encoding_size=cfg.dataset.video_encoding_batch_size, - vcodec=cfg.dataset.vcodec, + camera_encoder=cfg.dataset.camera_encoder, streaming_encoding=cfg.dataset.streaming_encoding, encoder_queue_maxsize=cfg.dataset.encoder_queue_maxsize, encoder_threads=cfg.dataset.encoder_threads, @@ -367,7 +367,7 @@ def build_rollout_context( image_writer_threads=cfg.dataset.num_image_writer_threads_per_camera * len(robot.cameras if hasattr(robot, "cameras") else []), batch_encoding_size=cfg.dataset.video_encoding_batch_size, - vcodec=cfg.dataset.vcodec, + camera_encoder=cfg.dataset.camera_encoder, streaming_encoding=cfg.dataset.streaming_encoding, encoder_queue_maxsize=cfg.dataset.encoder_queue_maxsize, encoder_threads=cfg.dataset.encoder_threads, diff --git a/src/lerobot/scripts/lerobot_edit_dataset.py b/src/lerobot/scripts/lerobot_edit_dataset.py index a708d37a3..eb6a57870 100644 --- a/src/lerobot/scripts/lerobot_edit_dataset.py +++ b/src/lerobot/scripts/lerobot_edit_dataset.py @@ -187,12 +187,12 @@ import abc import logging import shutil import sys -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path import draccus -from lerobot.configs import parser +from lerobot.configs import VideoEncoderConfig, camera_encoder_defaults, parser from lerobot.datasets import ( LeRobotDataset, convert_image_to_video_dataset, @@ -250,11 +250,7 @@ class ModifyTasksConfig(OperationConfig): @dataclass class ConvertImageToVideoConfig(OperationConfig): output_dir: str | None = None - vcodec: str = "libsvtav1" - pix_fmt: str = "yuv420p" - g: int = 2 - crf: int = 30 - fast_decode: int = 0 + camera_encoder: VideoEncoderConfig = field(default_factory=camera_encoder_defaults) episode_indices: list[int] | None = None num_workers: int = 4 max_episodes_per_batch: int | None = None @@ -557,11 +553,7 @@ def handle_convert_image_to_video(cfg: EditDatasetConfig) -> None: dataset=dataset, output_dir=output_dir, repo_id=output_repo_id, - vcodec=getattr(cfg.operation, "vcodec", "libsvtav1"), - pix_fmt=getattr(cfg.operation, "pix_fmt", "yuv420p"), - g=getattr(cfg.operation, "g", 2), - crf=getattr(cfg.operation, "crf", 30), - fast_decode=getattr(cfg.operation, "fast_decode", 0), + camera_encoder=getattr(cfg.operation, "camera_encoder", None) or camera_encoder_defaults(), episode_indices=getattr(cfg.operation, "episode_indices", None), num_workers=getattr(cfg.operation, "num_workers", 4), max_episodes_per_batch=getattr(cfg.operation, "max_episodes_per_batch", None), diff --git a/src/lerobot/scripts/lerobot_record.py b/src/lerobot/scripts/lerobot_record.py index 129696bd3..c8419cb14 100644 --- a/src/lerobot/scripts/lerobot_record.py +++ b/src/lerobot/scripts/lerobot_record.py @@ -63,6 +63,27 @@ lerobot-record \\ --dataset.streaming_encoding=true \\ --dataset.encoder_threads=2 ``` + +Example recording with custom video encoding parameters: +```shell +lerobot-record \\ + --robot.type=so100_follower \\ + --robot.port=/dev/tty.usbmodem58760431541 \\ + --robot.cameras="{laptop: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \\ + --robot.id=black \\ + --teleop.type=so100_leader \\ + --teleop.port=/dev/tty.usbmodem58760431551 \\ + --teleop.id=blue \\ + --dataset.repo_id=/ \\ + --dataset.num_episodes=2 \\ + --dataset.single_task="Grab the cube" \\ + --dataset.streaming_encoding=true \\ + --dataset.encoder_threads=2 \\ + --dataset.camera_encoder.vcodec=h264 \\ + --dataset.camera_encoder.preset=fast \\ + --dataset.camera_encoder.extra_options={"tune": "film", "profile:v": "high", "bf": 2} \\ + --display_data=true +``` """ import logging @@ -377,10 +398,10 @@ def record( cfg.dataset.repo_id, root=cfg.dataset.root, batch_encoding_size=cfg.dataset.video_encoding_batch_size, - vcodec=cfg.dataset.vcodec, + camera_encoder=cfg.dataset.camera_encoder, + encoder_threads=cfg.dataset.encoder_threads, streaming_encoding=cfg.dataset.streaming_encoding, encoder_queue_maxsize=cfg.dataset.encoder_queue_maxsize, - encoder_threads=cfg.dataset.encoder_threads, image_writer_processes=cfg.dataset.num_image_writer_processes if num_cameras > 0 else 0, image_writer_threads=cfg.dataset.num_image_writer_threads_per_camera * num_cameras if num_cameras > 0 @@ -406,10 +427,10 @@ def record( image_writer_processes=cfg.dataset.num_image_writer_processes, image_writer_threads=cfg.dataset.num_image_writer_threads_per_camera * len(robot.cameras), batch_encoding_size=cfg.dataset.video_encoding_batch_size, - vcodec=cfg.dataset.vcodec, + camera_encoder=cfg.dataset.camera_encoder, + encoder_threads=cfg.dataset.encoder_threads, streaming_encoding=cfg.dataset.streaming_encoding, encoder_queue_maxsize=cfg.dataset.encoder_queue_maxsize, - encoder_threads=cfg.dataset.encoder_threads, ) robot.connect() @@ -420,7 +441,7 @@ def record( if not cfg.dataset.streaming_encoding: logging.info( - "Streaming encoding is disabled. If you have capable hardware, consider enabling it for way faster episode saving. --dataset.streaming_encoding=true --dataset.encoder_threads=2 # --dataset.vcodec=auto. More info in the documentation: https://huggingface.co/docs/lerobot/streaming_video_encoding" + "Streaming encoding is disabled. If you have capable hardware, consider enabling it for way faster episode saving. --dataset.streaming_encoding=true --dataset.encoder_threads=2 # --dataset.camera_encoder.vcodec=auto. More info in the documentation: https://huggingface.co/docs/lerobot/streaming_video_encoding" ) with VideoEncodingManager(dataset): diff --git a/src/lerobot/scripts/lerobot_rollout.py b/src/lerobot/scripts/lerobot_rollout.py index 6a81563ee..7015e707c 100644 --- a/src/lerobot/scripts/lerobot_rollout.py +++ b/src/lerobot/scripts/lerobot_rollout.py @@ -120,6 +120,18 @@ Usage examples --dataset.repo_id=user/rollout_sentry_data \\ --dataset.single_task="patrol" \\ --resume=true + + # Rollout with custom video encoding parameters + lerobot-rollout \\ + --strategy.type=base \\ + --policy.path=lerobot/act_koch_real \\ + --robot.type=koch_follower \\ + --robot.port=/dev/ttyACM0 \\ + --task="pick up cube" --duration=60 \\ + --display_data=true \\ + --dataset.camera_encoder.vcodec=h264 \\ + --dataset.camera_encoder.preset=fast \\ + --dataset.camera_encoder.extra_options={"tune": "film", "profile:v": "high", "bf": 2} """ import logging diff --git a/src/lerobot/transport/utils.py b/src/lerobot/transport/utils.py index 8da338044..2ef63c2cc 100644 --- a/src/lerobot/transport/utils.py +++ b/src/lerobot/transport/utils.py @@ -25,9 +25,10 @@ from typing import Any import torch -from lerobot.transport import services_pb2 from lerobot.utils.transition import Transition +from . import services_pb2 + # FIX for protobuf: Assign the enum to a variable and ignore the type error once TransferState = services_pb2.TransferState # type: ignore[attr-defined] diff --git a/src/lerobot/utils/import_utils.py b/src/lerobot/utils/import_utils.py index 6ba912bf5..ef03367eb 100644 --- a/src/lerobot/utils/import_utils.py +++ b/src/lerobot/utils/import_utils.py @@ -69,7 +69,7 @@ def is_package_available( return package_exists -def get_safe_default_codec(): +def get_safe_default_video_backend(): logger = logging.getLogger(__name__) if importlib.util.find_spec("torchcodec"): return "torchcodec" @@ -128,6 +128,9 @@ _hidapi_available = is_package_available("hidapi", import_name="hid") _pandas_available = is_package_available("pandas") _faker_available = is_package_available("faker") +# Video encoding / decoding +_av_available = is_package_available("av") + # Misc _pynput_available = is_package_available("pynput") _pygame_available = is_package_available("pygame") diff --git a/tests/artifacts/encoded_videos/clip_32x48.mp4 b/tests/artifacts/encoded_videos/clip_32x48.mp4 new file mode 100644 index 000000000..086c399d3 --- /dev/null +++ b/tests/artifacts/encoded_videos/clip_32x48.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2191cd86e9e32ecbe18e33ad68d49060e479723ab5a3212bbb26df3025ccb568 +size 5815 diff --git a/tests/artifacts/encoded_videos/clip_4frames.mp4 b/tests/artifacts/encoded_videos/clip_4frames.mp4 new file mode 100644 index 000000000..487c3c8ad --- /dev/null +++ b/tests/artifacts/encoded_videos/clip_4frames.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e0ebf563ba3ed9c24b691a0f0b29e0294a1fa9b51422e1ece296155f1465768 +size 16236 diff --git a/tests/artifacts/encoded_videos/clip_5frames.mp4 b/tests/artifacts/encoded_videos/clip_5frames.mp4 new file mode 100644 index 000000000..cbbe81c39 --- /dev/null +++ b/tests/artifacts/encoded_videos/clip_5frames.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8475bfd5e6c4c780df46200e2b027e262b38436c57d01078bd943a5b87c65b8f +size 20726 diff --git a/tests/artifacts/encoded_videos/clip_6frames.mp4 b/tests/artifacts/encoded_videos/clip_6frames.mp4 new file mode 100644 index 000000000..50d9badca --- /dev/null +++ b/tests/artifacts/encoded_videos/clip_6frames.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6434322d1c671a7d132367619f841a775317cb9ff973f3f4505831e3ed74076d +size 23808 diff --git a/tests/artifacts/encoded_videos/clip_h264.mp4 b/tests/artifacts/encoded_videos/clip_h264.mp4 new file mode 100644 index 000000000..90698dcf5 --- /dev/null +++ b/tests/artifacts/encoded_videos/clip_h264.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8efc84375e92a3499cef93100e04d8fb354670f3d9e0db2097b52575927284fc +size 12237 diff --git a/tests/datasets/test_aggregate.py b/tests/datasets/test_aggregate.py index 6d646d4f7..80a95aa1f 100644 --- a/tests/datasets/test_aggregate.py +++ b/tests/datasets/test_aggregate.py @@ -14,6 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json +import logging from unittest.mock import patch import pytest @@ -23,7 +25,9 @@ pytest.importorskip("datasets", reason="datasets is required (install lerobot[da import datasets # noqa: E402 import torch +from lerobot.configs import VIDEO_ENCODER_INFO_KEYS from lerobot.datasets.aggregate import aggregate_datasets +from lerobot.datasets.feature_utils import features_equal_for_merge from lerobot.datasets.lerobot_dataset import LeRobotDataset from tests.fixtures.constants import DUMMY_REPO_ID @@ -117,8 +121,9 @@ def assert_metadata_consistency(aggr_ds, ds_0, ds_1): "Robot type should be the same" ) - # Test features are the same - assert aggr_ds.features == ds_0.features == ds_1.features, "Features should be the same" + # Schema matches; merged video ``info`` is reconciled separately from per-source ``info``. + assert features_equal_for_merge(aggr_ds.features, ds_0.features) + assert features_equal_for_merge(aggr_ds.features, ds_1.features) # Test tasks aggregation expected_tasks = set(ds_0.meta.tasks.index) | set(ds_1.meta.tasks.index) @@ -284,6 +289,73 @@ def test_aggregate_datasets(tmp_path, lerobot_dataset_factory): assert_dataset_iteration_works(aggr_ds) +@pytest.mark.parametrize("mutation", ["mismatched_value", "missing_key"]) +def test_aggregate_incomplete_video_encoder_info_warns_and_nuls_encoders( + tmp_path, lerobot_dataset_factory, caplog, mutation +): + """Mismatched or missing encoder ``info`` is merged per-key with fallbacks and a warning.""" + suffix = "enc_mismatch" if mutation == "mismatched_value" else "enc_missing" + ds_0 = lerobot_dataset_factory( + root=tmp_path / f"{suffix}_a", + repo_id=f"{DUMMY_REPO_ID}_{suffix}_a", + total_episodes=2, + total_frames=20, + ) + ds_1 = lerobot_dataset_factory( + root=tmp_path / f"{suffix}_b", + repo_id=f"{DUMMY_REPO_ID}_{suffix}_b", + total_episodes=2, + total_frames=20, + ) + + info_path = ds_1.root / "meta" / "info.json" + data = json.loads(info_path.read_text()) + for ft in data["features"].values(): + if ft.get("dtype") != "video": + continue + inf = ft.setdefault("info", {}) + if mutation == "mismatched_value": + inf["video.crf"] = 99 + inf["video.extra_options"] = {"tune": "film"} + else: + inf.pop("video.crf", None) + inf.pop("video.extra_options", None) + info_path.write_text(json.dumps(data)) + + aggr_id = f"{DUMMY_REPO_ID}_{suffix}_aggr" + aggr_root = tmp_path / f"{suffix}_aggr" + with caplog.at_level(logging.WARNING): + aggregate_datasets( + repo_ids=[ds_0.repo_id, ds_1.repo_id], + roots=[ds_0.root, ds_1.root], + aggr_repo_id=aggr_id, + aggr_root=aggr_root, + ) + + assert "heterogeneous" in caplog.text.lower() or "incomplete" in caplog.text.lower() + + with ( + patch("lerobot.datasets.dataset_metadata.get_safe_version") as mock_get_safe_version, + patch("lerobot.datasets.dataset_metadata.snapshot_download") as mock_snapshot_download, + ): + mock_get_safe_version.return_value = "v3.0" + mock_snapshot_download.return_value = str(aggr_root) + aggr_ds = LeRobotDataset(aggr_id, root=aggr_root) + + for key, ft in aggr_ds.meta.info.features.items(): + if ft.get("dtype") != "video": + continue + info = ft["info"] + reference = ds_0.meta.info.features[key]["info"] + for info_key in VIDEO_ENCODER_INFO_KEYS: + if info_key == "video.crf": + assert info[info_key] is None + elif info_key == "video.extra_options": + assert info[info_key] == {} + else: + assert info[info_key] == reference[info_key] + + def test_aggregate_with_low_threshold(tmp_path, lerobot_dataset_factory): """Test aggregation with small file size limits to force file rotation/sharding.""" ds_0_num_episodes = ds_1_num_episodes = 10 diff --git a/tests/datasets/test_dataset_reader.py b/tests/datasets/test_dataset_reader.py index bbe858b5d..085563bb8 100644 --- a/tests/datasets/test_dataset_reader.py +++ b/tests/datasets/test_dataset_reader.py @@ -20,7 +20,7 @@ import pytest pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])") from lerobot.datasets.dataset_reader import DatasetReader -from lerobot.utils.import_utils import get_safe_default_codec +from lerobot.utils.import_utils import get_safe_default_video_backend # ── Loading ────────────────────────────────────────────────────────── @@ -35,7 +35,7 @@ def test_try_load_returns_true_when_data_exists(tmp_path, lerobot_dataset_factor root=dataset.root, episodes=None, tolerance_s=1e-4, - video_backend=get_safe_default_codec(), + video_backend=get_safe_default_video_backend(), delta_timestamps=None, image_transforms=None, ) @@ -58,7 +58,7 @@ def test_try_load_returns_false_when_no_data(tmp_path): root=meta.root, episodes=None, tolerance_s=1e-4, - video_backend=get_safe_default_codec(), + video_backend=get_safe_default_video_backend(), delta_timestamps=None, image_transforms=None, ) diff --git a/tests/datasets/test_dataset_tools.py b/tests/datasets/test_dataset_tools.py index 0b0862f00..032fd4f7c 100644 --- a/tests/datasets/test_dataset_tools.py +++ b/tests/datasets/test_dataset_tools.py @@ -23,8 +23,10 @@ import torch pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])") +from lerobot.configs import VideoEncoderConfig from lerobot.datasets.dataset_tools import ( add_features, + convert_image_to_video_dataset, delete_episodes, merge_datasets, modify_features, @@ -32,7 +34,6 @@ from lerobot.datasets.dataset_tools import ( remove_feature, split_dataset, ) -from lerobot.scripts.lerobot_edit_dataset import convert_image_to_video_dataset @pytest.fixture @@ -1246,10 +1247,12 @@ def test_convert_image_to_video_dataset(tmp_path): dataset=source_dataset, output_dir=output_dir, repo_id="lerobot/pusht_video", - vcodec="libsvtav1", - pix_fmt="yuv420p", - g=2, - crf=30, + camera_encoder=VideoEncoderConfig( + vcodec="libsvtav1", + pix_fmt="yuv420p", + g=2, + crf=30, + ), episode_indices=[0, 1], num_workers=2, ) diff --git a/tests/datasets/test_dataset_writer.py b/tests/datasets/test_dataset_writer.py index 8d2bc0373..8670aeebc 100644 --- a/tests/datasets/test_dataset_writer.py +++ b/tests/datasets/test_dataset_writer.py @@ -25,6 +25,7 @@ from PIL import Image pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])") +from lerobot.configs import VideoEncoderConfig from lerobot.datasets.dataset_writer import _encode_video_worker from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.datasets.utils import DEFAULT_IMAGE_PATH @@ -52,8 +53,8 @@ def _make_frame(features: dict, task: str = "Dummy task") -> dict: # ── Existing encode_video_worker tests ─────────────────────────────── -def test_encode_video_worker_forwards_vcodec(tmp_path): - """_encode_video_worker correctly forwards the vcodec parameter.""" +def test_encode_video_worker_forwards_camera_encoder(tmp_path): + """_encode_video_worker forwards camera_encoder to encode_video_frames.""" video_key = "observation.images.laptop" fpath = DEFAULT_IMAGE_PATH.format(image_key=video_key, episode_index=0, frame_index=0) img_dir = tmp_path / Path(fpath).parent @@ -68,13 +69,21 @@ def test_encode_video_worker_forwards_vcodec(tmp_path): Path(video_path).touch() with patch("lerobot.datasets.dataset_writer.encode_video_frames", side_effect=mock_encode): - _encode_video_worker(video_key, 0, tmp_path, fps=30, vcodec="h264") + _encode_video_worker( + video_key, + 0, + tmp_path, + fps=30, + camera_encoder=VideoEncoderConfig(vcodec="h264", preset=None), + encoder_threads=4, + ) - assert captured_kwargs["vcodec"] == "h264" + assert captured_kwargs["camera_encoder"].vcodec == "h264" + assert captured_kwargs["encoder_threads"] == 4 -def test_encode_video_worker_default_vcodec(tmp_path): - """_encode_video_worker uses libsvtav1 as the default codec.""" +def test_encode_video_worker_default_camera_encoder(tmp_path): + """_encode_video_worker passes None camera_encoder which encode_video_frames defaults.""" video_key = "observation.images.laptop" fpath = DEFAULT_IMAGE_PATH.format(image_key=video_key, episode_index=0, frame_index=0) img_dir = tmp_path / Path(fpath).parent @@ -91,7 +100,8 @@ def test_encode_video_worker_default_vcodec(tmp_path): with patch("lerobot.datasets.dataset_writer.encode_video_frames", side_effect=mock_encode): _encode_video_worker(video_key, 0, tmp_path, fps=30) - assert captured_kwargs["vcodec"] == "libsvtav1" + assert captured_kwargs["camera_encoder"] is None + assert captured_kwargs["encoder_threads"] is None # ── add_frame contracts ────────────────────────────────────────────── diff --git a/tests/datasets/test_datasets.py b/tests/datasets/test_datasets.py index 654f8cdf1..ba9b64812 100644 --- a/tests/datasets/test_datasets.py +++ b/tests/datasets/test_datasets.py @@ -29,6 +29,7 @@ from PIL import Image from safetensors.torch import load_file from torchvision.transforms import v2 +from lerobot.configs import VALID_VIDEO_CODECS, VideoEncoderConfig from lerobot.configs.default import DatasetConfig from lerobot.configs.train import TrainPipelineConfig from lerobot.datasets import make_dataset @@ -43,7 +44,6 @@ from lerobot.datasets.utils import ( DEFAULT_VIDEO_FILE_SIZE_IN_MB, create_branch, ) -from lerobot.datasets.video_utils import VALID_VIDEO_CODECS from lerobot.envs.factory import make_env_config from lerobot.policies.factory import make_policy_config from lerobot.robots import make_robot_from_config @@ -1470,17 +1470,9 @@ def test_frames_in_current_file_calculation(tmp_path, empty_lerobot_dataset_fact def test_lerobot_dataset_vcodec_validation(): - """Test that LeRobotDataset validates the vcodec parameter.""" - # Test that invalid vcodec raises ValueError + """Invalid vcodec in encoder config is rejected at construction time.""" with pytest.raises(ValueError, match="Invalid vcodec"): - LeRobotDataset.__new__(LeRobotDataset) # bypass __init__ to test validation directly - # Actually test via create since it's easier - LeRobotDataset.create( - repo_id="test/invalid_codec", - fps=30, - features={"observation.state": {"dtype": "float32", "shape": (2,), "names": ["x", "y"]}}, - vcodec="invalid_codec", - ) + VideoEncoderConfig(vcodec="invalid_codec") def test_valid_video_codecs_constant(): diff --git a/tests/datasets/test_streaming_video_encoder.py b/tests/datasets/test_streaming_video_encoder.py index 8b7a1540f..b69f24254 100644 --- a/tests/datasets/test_streaming_video_encoder.py +++ b/tests/datasets/test_streaming_video_encoder.py @@ -14,11 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for streaming video encoding and hardware-accelerated encoding.""" +"""Tests for streaming video encoding.""" import queue import threading -from unittest.mock import patch import numpy as np import pytest @@ -27,112 +26,20 @@ pytest.importorskip("av", reason="av is required (install lerobot[dataset])") import av # noqa: E402 +from lerobot.configs import VideoEncoderConfig +from lerobot.datasets.pyav_utils import get_codec from lerobot.datasets.video_utils import ( - VALID_VIDEO_CODECS, StreamingVideoEncoder, _CameraEncoderThread, - _get_codec_options, - detect_available_hw_encoders, - resolve_vcodec, ) from lerobot.utils.constants import OBS_IMAGES -# ─── _get_codec_options tests ─── - - -class TestGetCodecOptions: - def test_libsvtav1_defaults(self): - opts = _get_codec_options("libsvtav1") - assert opts["g"] == "2" - assert opts["crf"] == "30" - assert opts["preset"] == "12" - - def test_libsvtav1_custom_preset(self): - opts = _get_codec_options("libsvtav1", preset=8) - assert opts["preset"] == "8" - - def test_h264_options(self): - opts = _get_codec_options("h264", g=10, crf=23) - assert opts["g"] == "10" - assert opts["crf"] == "23" - assert "preset" not in opts - - def test_videotoolbox_options(self): - opts = _get_codec_options("h264_videotoolbox", g=2, crf=30) - assert opts["g"] == "2" - # CRF 30 maps to quality = max(1, min(100, 100 - 30*2)) = 40 - assert opts["q:v"] == "40" - assert "crf" not in opts - - def test_nvenc_options(self): - opts = _get_codec_options("h264_nvenc", g=2, crf=25) - assert opts["rc"] == "constqp" - assert opts["qp"] == "25" - assert "crf" not in opts - # NVENC doesn't support g - assert "g" not in opts - - def test_vaapi_options(self): - opts = _get_codec_options("h264_vaapi", crf=28) - assert opts["qp"] == "28" - - def test_qsv_options(self): - opts = _get_codec_options("h264_qsv", crf=25) - assert opts["global_quality"] == "25" - - def test_no_g_no_crf(self): - opts = _get_codec_options("h264", g=None, crf=None) - assert "g" not in opts - assert "crf" not in opts - - -# ─── HW encoder detection tests ─── - - -class TestHWEncoderDetection: - def test_detect_available_hw_encoders_returns_list(self): - result = detect_available_hw_encoders() - assert isinstance(result, list) - - def test_detect_available_hw_encoders_only_valid(self): - from lerobot.datasets.video_utils import HW_ENCODERS - - result = detect_available_hw_encoders() - for encoder in result: - assert encoder in HW_ENCODERS - - def test_resolve_vcodec_passthrough(self): - assert resolve_vcodec("libsvtav1") == "libsvtav1" - assert resolve_vcodec("h264") == "h264" - - def test_resolve_vcodec_auto_fallback(self): - """When no HW encoders are available, auto should fall back to libsvtav1.""" - with patch("lerobot.datasets.video_utils.detect_available_hw_encoders", return_value=[]): - assert resolve_vcodec("auto") == "libsvtav1" - - def test_resolve_vcodec_auto_picks_hw(self): - """When a HW encoder is available, auto should pick it.""" - with patch( - "lerobot.datasets.video_utils.detect_available_hw_encoders", - return_value=["h264_videotoolbox"], - ): - assert resolve_vcodec("auto") == "h264_videotoolbox" - - def test_resolve_vcodec_auto_returns_valid(self): - """Test that resolve_vcodec('auto') returns a known valid codec.""" - result = resolve_vcodec("auto") - assert result in VALID_VIDEO_CODECS - - def test_hw_encoder_names_accepted_in_validation(self): - """Test that HW encoder names pass validation in VALID_VIDEO_CODECS.""" - assert "auto" in VALID_VIDEO_CODECS - assert "h264_videotoolbox" in VALID_VIDEO_CODECS - assert "h264_nvenc" in VALID_VIDEO_CODECS - - def test_resolve_vcodec_invalid_raises(self): - """Test that resolve_vcodec raises ValueError for invalid codecs.""" - with pytest.raises(ValueError, match="Invalid vcodec"): - resolve_vcodec("not_a_real_codec") +# Cross-codec validation tests only fire when the target codec is present +# in the local FFmpeg build; on other platforms validate() is a no-op. +_has_videotoolbox = get_codec("h264_videotoolbox") is not None +_videotoolbox_only = pytest.mark.skipif( + not _has_videotoolbox, reason="h264_videotoolbox not in local FFmpeg build" +) # ─── _CameraEncoderThread tests ─── @@ -150,14 +57,13 @@ class TestCameraEncoderThread: result_queue: queue.Queue = queue.Queue(maxsize=1) stop_event = threading.Event() + enc_cfg = VideoEncoderConfig(vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30, preset=13) encoder_thread = _CameraEncoderThread( video_path=video_path, fps=fps, - vcodec="libsvtav1", - pix_fmt="yuv420p", - g=2, - crf=30, - preset=13, + vcodec=enc_cfg.vcodec, + pix_fmt=enc_cfg.pix_fmt, + codec_options=enc_cfg.get_codec_options(as_strings=True), frame_queue=frame_queue, result_queue=result_queue, stop_event=stop_event, @@ -202,14 +108,13 @@ class TestCameraEncoderThread: result_queue: queue.Queue = queue.Queue(maxsize=1) stop_event = threading.Event() + enc_cfg = VideoEncoderConfig(vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30, preset=13) encoder_thread = _CameraEncoderThread( video_path=video_path, fps=fps, - vcodec="libsvtav1", - pix_fmt="yuv420p", - g=2, - crf=30, - preset=13, + vcodec=enc_cfg.vcodec, + pix_fmt=enc_cfg.pix_fmt, + codec_options=enc_cfg.get_codec_options(as_strings=True), frame_queue=frame_queue, result_queue=result_queue, stop_event=stop_event, @@ -237,14 +142,13 @@ class TestCameraEncoderThread: result_queue: queue.Queue = queue.Queue(maxsize=1) stop_event = threading.Event() + enc_cfg = VideoEncoderConfig(vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30, preset=13) encoder_thread = _CameraEncoderThread( video_path=video_path, fps=fps, - vcodec="libsvtav1", - pix_fmt="yuv420p", - g=2, - crf=30, - preset=13, + vcodec=enc_cfg.vcodec, + pix_fmt=enc_cfg.pix_fmt, + codec_options=enc_cfg.get_codec_options(as_strings=True), frame_queue=frame_queue, result_queue=result_queue, stop_event=stop_event, @@ -266,11 +170,20 @@ class TestCameraEncoderThread: class TestStreamingVideoEncoder: + def _make_encoder_config(self, **kwargs): + """Helper to build a VideoEncoderConfig.""" + return VideoEncoderConfig(**kwargs) + def test_single_camera_episode(self, tmp_path): """Test encoding a single camera episode.""" - encoder = StreamingVideoEncoder(fps=30, vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30, preset=13) - video_keys = [f"{OBS_IMAGES}.laptop"] + encoder = StreamingVideoEncoder( + fps=30, + camera_encoder=self._make_encoder_config( + vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30, preset=13 + ), + ) + encoder.start_episode(video_keys, tmp_path) num_frames = 20 @@ -295,9 +208,11 @@ class TestStreamingVideoEncoder: def test_multi_camera_episode(self, tmp_path): """Test encoding multiple cameras simultaneously.""" - encoder = StreamingVideoEncoder(fps=30, vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30) - video_keys = [f"{OBS_IMAGES}.laptop", f"{OBS_IMAGES}.phone"] + encoder = StreamingVideoEncoder( + fps=30, + camera_encoder=self._make_encoder_config(vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30), + ) encoder.start_episode(video_keys, tmp_path) num_frames = 15 @@ -319,8 +234,11 @@ class TestStreamingVideoEncoder: def test_sequential_episodes(self, tmp_path): """Test that multiple sequential episodes work correctly.""" - encoder = StreamingVideoEncoder(fps=30, vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30) video_keys = [f"{OBS_IMAGES}.cam"] + encoder = StreamingVideoEncoder( + fps=30, + camera_encoder=self._make_encoder_config(vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30), + ) for ep in range(3): encoder.start_episode(video_keys, tmp_path) @@ -342,8 +260,11 @@ class TestStreamingVideoEncoder: def test_cancel_episode(self, tmp_path): """Test that canceling an episode cleans up properly.""" - encoder = StreamingVideoEncoder(fps=30, vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30) video_keys = [f"{OBS_IMAGES}.cam"] + encoder = StreamingVideoEncoder( + fps=30, + camera_encoder=self._make_encoder_config(vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30), + ) encoder.start_episode(video_keys, tmp_path) @@ -365,28 +286,33 @@ class TestStreamingVideoEncoder: def test_feed_without_start_raises(self, tmp_path): """Test that feeding frames without starting an episode raises.""" - encoder = StreamingVideoEncoder(fps=30, vcodec="libsvtav1", pix_fmt="yuv420p") + encoder = StreamingVideoEncoder(fps=30) with pytest.raises(RuntimeError, match="No active episode"): encoder.feed_frame("cam", np.zeros((64, 96, 3), dtype=np.uint8)) encoder.close() def test_finish_without_start_raises(self, tmp_path): """Test that finishing without starting raises.""" - encoder = StreamingVideoEncoder(fps=30, vcodec="libsvtav1", pix_fmt="yuv420p") + encoder = StreamingVideoEncoder(fps=30) with pytest.raises(RuntimeError, match="No active episode"): encoder.finish_episode() encoder.close() def test_close_is_idempotent(self, tmp_path): """Test that close() can be called multiple times safely.""" - encoder = StreamingVideoEncoder(fps=30, vcodec="libsvtav1", pix_fmt="yuv420p") + encoder = StreamingVideoEncoder(fps=30) encoder.close() encoder.close() # Should not raise def test_video_duration_matches_frame_count(self, tmp_path): """Test that encoded video duration matches num_frames / fps.""" - encoder = StreamingVideoEncoder(fps=30, vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30, preset=13) video_keys = [f"{OBS_IMAGES}.cam"] + encoder = StreamingVideoEncoder( + fps=30, + camera_encoder=self._make_encoder_config( + vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30, preset=13 + ), + ) encoder.start_episode(video_keys, tmp_path) num_frames = 90 # 3 seconds at 30fps @@ -417,9 +343,11 @@ class TestStreamingVideoEncoder: def test_multi_camera_start_episode_called_once(self, tmp_path): """Test that with multiple cameras, no frames are lost due to double start_episode.""" - encoder = StreamingVideoEncoder(fps=30, vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30) - video_keys = [f"{OBS_IMAGES}.cam1", f"{OBS_IMAGES}.cam2"] + encoder = StreamingVideoEncoder( + fps=30, + camera_encoder=self._make_encoder_config(vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30), + ) encoder.start_episode(video_keys, tmp_path) num_frames = 30 @@ -446,17 +374,24 @@ class TestStreamingVideoEncoder: def test_encoder_threads_passed_to_thread(self, tmp_path): """Test that encoder_threads is stored and passed through to encoder threads.""" - encoder = StreamingVideoEncoder( - fps=30, vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30, encoder_threads=2 - ) - assert encoder.encoder_threads == 2 - video_keys = [f"{OBS_IMAGES}.cam"] + cfg = VideoEncoderConfig( + vcodec="libsvtav1", + pix_fmt="yuv420p", + g=2, + crf=30, + ) + encoder = StreamingVideoEncoder( + fps=30, + camera_encoder=cfg, + encoder_threads=2, + ) + assert encoder._encoder_threads == 2 encoder.start_episode(video_keys, tmp_path) - # Verify the thread received the encoder_threads value + # Verify codec options include thread tuning for libsvtav1 (lp=…) thread = encoder._threads[f"{OBS_IMAGES}.cam"] - assert thread.encoder_threads == 2 + assert "svtav1-params" in thread.codec_options or "threads" in thread.codec_options # Feed some frames and finish to ensure it works end-to-end num_frames = 10 @@ -478,16 +413,20 @@ class TestStreamingVideoEncoder: def test_encoder_threads_none_by_default(self, tmp_path): """Test that encoder_threads defaults to None (codec auto-detect).""" - encoder = StreamingVideoEncoder(fps=30, vcodec="libsvtav1", pix_fmt="yuv420p") - assert encoder.encoder_threads is None + encoder = StreamingVideoEncoder(fps=30) + assert encoder._encoder_threads is None encoder.close() def test_graceful_frame_dropping(self, tmp_path): """Test that full queue drops frames instead of crashing.""" - encoder = StreamingVideoEncoder( - fps=30, vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30, preset=13, queue_maxsize=1 - ) video_keys = [f"{OBS_IMAGES}.cam"] + encoder = StreamingVideoEncoder( + fps=30, + camera_encoder=self._make_encoder_config( + vcodec="libsvtav1", pix_fmt="yuv420p", g=2, crf=30, preset=13 + ), + queue_maxsize=1, + ) encoder.start_episode(video_keys, tmp_path) # Feed many frames quickly - with queue_maxsize=1, some will be dropped diff --git a/tests/datasets/test_video_encoding.py b/tests/datasets/test_video_encoding.py new file mode 100644 index 000000000..224f2405b --- /dev/null +++ b/tests/datasets/test_video_encoding.py @@ -0,0 +1,595 @@ +#!/usr/bin/env python + +# Copyright 2026 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for ``lerobot.datasets.video_utils`` encoding functions and ``lerobot.configs.video.VideoEncoderConfig`` config class.""" + +import json +from pathlib import Path + +import numpy as np +import pytest + +pytest.importorskip("av", reason="av is required (install lerobot[dataset])") + +import av # noqa: E402 + +from lerobot.configs import VALID_VIDEO_CODECS, VideoEncoderConfig +from lerobot.datasets.image_writer import write_image +from lerobot.datasets.lerobot_dataset import LeRobotDataset +from lerobot.datasets.pyav_utils import get_codec +from lerobot.datasets.utils import INFO_PATH +from lerobot.datasets.video_utils import ( + concatenate_video_files, + encode_video_frames, + get_video_info, +) +from tests.fixtures.constants import DUMMY_VIDEO_INFO + + +# Per-codec skip markers — validation tests only fire when the codec is available +def _require_encoder(vcodec: str) -> pytest.MarkDecorator: + """Skip the test if ``vcodec`` is not available in the local FFmpeg build.""" + return pytest.mark.skipif(get_codec(vcodec) is None, reason=f"{vcodec!r} not in local FFmpeg build") + + +require_libsvtav1 = _require_encoder("libsvtav1") +require_h264 = _require_encoder("h264") +require_videotoolbox = _require_encoder("h264_videotoolbox") +require_nvenc = _require_encoder("h264_nvenc") +require_vaapi = _require_encoder("h264_vaapi") +require_qsv = _require_encoder("h264_qsv") + + +# ─── VideoEncoderConfig / codec options ────────────────────────────── + + +class TestCodecOptions: + @require_libsvtav1 + def test_libsvtav1_defaults(self): + cfg = VideoEncoderConfig() + opts = cfg.get_codec_options() + assert opts["g"] == 2 + assert opts["crf"] == 30 + assert opts["preset"] == 12 + + @require_libsvtav1 + def test_libsvtav1_custom_preset(self): + cfg = VideoEncoderConfig(preset=8) + assert cfg.get_codec_options()["preset"] == 8 + + @require_h264 + def test_h264_options(self): + cfg = VideoEncoderConfig(vcodec="h264", g=10, crf=23, preset=None) + opts = cfg.get_codec_options() + assert opts["g"] == 10 + assert opts["crf"] == 23 + assert "preset" not in opts + + @require_videotoolbox + def test_videotoolbox_options(self): + cfg = VideoEncoderConfig(vcodec="h264_videotoolbox", g=2, crf=30, preset=None) + opts = cfg.get_codec_options() + assert opts["g"] == 2 + assert opts["q:v"] == 40 + assert "crf" not in opts + + @_require_encoder("h264_nvenc") + def test_nvenc_options(self): + cfg = VideoEncoderConfig(vcodec="h264_nvenc", g=2, crf=25, preset=None) + opts = cfg.get_codec_options() + assert opts["rc"] == 0 + assert opts["qp"] == 25 + assert "crf" not in opts + assert opts["g"] == 2 + + @_require_encoder("h264_vaapi") + def test_vaapi_options(self): + cfg = VideoEncoderConfig(vcodec="h264_vaapi", crf=28, preset=None) + assert cfg.get_codec_options()["qp"] == 28 + + @_require_encoder("h264_qsv") + def test_qsv_options(self): + cfg = VideoEncoderConfig(vcodec="h264_qsv", crf=25, preset=None) + assert cfg.get_codec_options()["global_quality"] == 25 + + @require_h264 + def test_no_g_no_crf(self): + cfg = VideoEncoderConfig(vcodec="h264", g=None, crf=None, preset=None) + opts = cfg.get_codec_options() + assert "g" not in opts + assert "crf" not in opts + + @require_libsvtav1 + def test_encoder_threads_libsvtav1(self): + cfg = VideoEncoderConfig(fast_decode=0) + opts = cfg.get_codec_options(encoder_threads=4) + assert "lp=4" in opts.get("svtav1-params", "") + + @require_h264 + def test_encoder_threads_h264(self): + cfg = VideoEncoderConfig(vcodec="h264", preset=None) + assert cfg.get_codec_options(encoder_threads=2)["threads"] == 2 + + @require_libsvtav1 + def test_fast_decode_libsvtav1(self): + cfg = VideoEncoderConfig(fast_decode=1) + opts = cfg.get_codec_options() + assert "fast-decode=1" in opts.get("svtav1-params", "") + + @require_libsvtav1 + def test_libsvtav1_fast_decode_clamped_to_svt_range(self): + """Out-of-range fast_decode is clamped to [0, 2] in svtav1-params (SVT-AV1 FastDecode).""" + cfg = VideoEncoderConfig(fast_decode=100) + assert "fast-decode=2" in cfg.get_codec_options().get("svtav1-params", "") + cfg_neg = VideoEncoderConfig(fast_decode=-5) + assert "fast-decode=0" in cfg_neg.get_codec_options().get("svtav1-params", "") + + @require_h264 + def test_fast_decode_h264(self): + cfg = VideoEncoderConfig(vcodec="h264", fast_decode=1, preset=None) + assert cfg.get_codec_options()["tune"] == "fastdecode" + + @require_libsvtav1 + def test_pix_fmt_unsupported_raises(self): + """Passing an unsupported pix_fmt is a hard error.""" + with pytest.raises(ValueError, match="pix_fmt"): + VideoEncoderConfig(pix_fmt="yuv444p") # libsvtav1 only supports yuv420p variants + + @require_libsvtav1 + @require_h264 + def test_preset_default_behaviour(self): + """Empty constructor picks preset=12 (libsvtav1 path); other codecs stay None.""" + assert VideoEncoderConfig().preset == 12 + assert VideoEncoderConfig(vcodec="libsvtav1").preset == 12 + assert VideoEncoderConfig(vcodec="h264").preset is None + assert VideoEncoderConfig(vcodec="h264", preset=None).preset is None + + @require_h264 + def test_preset_string_on_h264(self): + """h264 accepts string presets and forwards them to FFmpeg.""" + cfg = VideoEncoderConfig(vcodec="h264", preset="slow") + assert cfg.get_codec_options()["preset"] == "slow" + + @require_videotoolbox + def test_preset_on_videotoolbox_not_set(self): + """videotoolbox has no preset option at all.""" + cfg = VideoEncoderConfig(vcodec="h264_videotoolbox", preset="slow") + assert "preset" not in cfg.get_codec_options() + + @require_libsvtav1 + def test_libsvtav1_preset_out_of_range_raises(self): + """libsvtav1 preset must sit in [-2, 13] as exposed by PyAV.""" + with pytest.raises(ValueError, match="out of range"): + VideoEncoderConfig(vcodec="libsvtav1", preset=100) + with pytest.raises(ValueError, match="out of range"): + VideoEncoderConfig(vcodec="libsvtav1", preset=-3) + + @require_libsvtav1 + def test_libsvtav1_crf_out_of_range_raises(self): + """libsvtav1 crf must sit in [0, 63].""" + with pytest.raises(ValueError, match="crf.*out of range"): + VideoEncoderConfig(vcodec="libsvtav1", crf=64) + + @require_libsvtav1 + def test_libsvtav1_crf_rejects_python_float(self): + """libsvtav1 exposes ``crf`` as an INT AVOption; Python float must not pass validation.""" + with pytest.raises(ValueError, match="float values are not allowed"): + VideoEncoderConfig(vcodec="libsvtav1", crf=2.5) + + @require_libsvtav1 + def test_libsvtav1_extra_crf_rejects_fractional_string(self): + """INT options reject fractional values even when supplied only via ``extra_options``.""" + with pytest.raises(ValueError, match="float values are not allowed"): + VideoEncoderConfig( + vcodec="libsvtav1", + crf=None, + extra_options={"crf": "2.5"}, + ) + + @require_libsvtav1 + def test_libsvtav1_extra_crf_rejects_float(self): + with pytest.raises(ValueError, match="float values are not allowed"): + VideoEncoderConfig( + vcodec="libsvtav1", + crf=None, + extra_options={"crf": 2.5}, + ) + + @require_h264 + def test_h264_crf_accepts_float_and_int(self): + """x264 exposes crf as a FLOAT option, so both int and float are accepted.""" + assert VideoEncoderConfig(vcodec="h264", crf=23).get_codec_options()["crf"] == 23 + assert VideoEncoderConfig(vcodec="h264", crf=23.5).get_codec_options()["crf"] == 23.5 + + @require_libsvtav1 + def test_validate_is_rerunnable(self): + """After mutating a field, validate() re-checks and surfaces new issues.""" + cfg = VideoEncoderConfig(vcodec="libsvtav1") + cfg.preset = 100 # now out of range + with pytest.raises(ValueError, match="out of range"): + cfg.validate() + + +class TestExtraOptions: + @require_libsvtav1 + def test_default_is_empty_dict(self): + cfg = VideoEncoderConfig() + assert cfg.extra_options == {} + + @require_libsvtav1 + def test_unknown_key_passes_through(self): + """Keys not published as AVOptions are forwarded to FFmpeg.""" + cfg = VideoEncoderConfig(extra_options={"totally_made_up_option": "value"}) + assert cfg.extra_options == {"totally_made_up_option": "value"} + + @require_libsvtav1 + def test_numeric_value_in_range_ok(self): + """libsvtav1 exposes ``qp`` as INT in [0, 63].""" + cfg = VideoEncoderConfig(extra_options={"qp": 30}) + assert cfg.extra_options == {"qp": 30} + + @require_libsvtav1 + def test_numeric_out_of_range_raises(self): + with pytest.raises(ValueError, match=r"qp=.*out of range"): + VideoEncoderConfig(extra_options={"qp": 999}) + + @require_libsvtav1 + def test_numeric_string_accepted_in_range(self): + """Numeric strings are accepted for numeric options (mirrors FFmpeg).""" + cfg = VideoEncoderConfig(extra_options={"qp": "18"}) + assert cfg.extra_options == {"qp": "18"} + + @require_libsvtav1 + def test_numeric_string_out_of_range_raises(self): + with pytest.raises(ValueError, match=r"qp=.*out of range"): + VideoEncoderConfig(extra_options={"qp": "999"}) + + @require_libsvtav1 + def test_non_numeric_string_on_numeric_option_raises(self): + with pytest.raises(ValueError, match=r"qp=.*not numeric"): + VideoEncoderConfig(extra_options={"qp": "medium"}) + + @require_libsvtav1 + def test_bool_on_numeric_option_raises(self): + """``bool`` is explicitly rejected for numeric options.""" + with pytest.raises(ValueError, match=r"qp=.*not numeric"): + VideoEncoderConfig(extra_options={"qp": True}) + + @require_h264 + def test_string_option_passes_through_unchecked(self): + """String-typed AVOptions are NOT enum-checked (too many accept freeform).""" + cfg = VideoEncoderConfig(vcodec="h264", preset=None, extra_options={"tune": "some-future-tune"}) + assert cfg.extra_options == {"tune": "some-future-tune"} + + @require_libsvtav1 + def test_merged_into_codec_options_and_stringified(self): + """Typed merge by default; ``as_strings=True`` matches FFmpeg option dict.""" + cfg = VideoEncoderConfig(extra_options={"qp": 20}) + opts = cfg.get_codec_options() + assert opts["qp"] == 20 + assert isinstance(opts["qp"], int) + assert cfg.get_codec_options(as_strings=True)["qp"] == "20" + + @require_libsvtav1 + def test_structured_fields_win_on_collision(self): + """A colliding extra_options key is discarded; the structured field wins.""" + cfg = VideoEncoderConfig(crf=30, extra_options={"crf": 18}) + assert cfg.get_codec_options()["crf"] == 30 + + +class TestEncoderDetection: + @require_h264 + def test_explicit_codec_kept_when_available(self): + cfg = VideoEncoderConfig(vcodec="h264") + assert cfg.vcodec == "h264" + + @require_videotoolbox + def test_auto_picks_videotoolbox_when_available(self): + """``h264_videotoolbox`` sits at the top of ``HW_VIDEO_CODECS`` so it wins when present.""" + cfg = VideoEncoderConfig(vcodec="auto") + assert cfg.vcodec == "h264_videotoolbox" + + def test_invalid_codec_raises(self): + with pytest.raises(ValueError, match="Invalid vcodec"): + VideoEncoderConfig(vcodec="not_a_real_codec") + + def test_hw_encoder_names_listed_as_valid(self): + assert "auto" in VALID_VIDEO_CODECS + assert "h264_videotoolbox" in VALID_VIDEO_CODECS + assert "h264_nvenc" in VALID_VIDEO_CODECS + + +TEST_ARTIFACTS_DIR = Path(__file__).parent.parent / "artifacts" / "encoded_videos" + +# Default video feature set used by persistence tests. +VIDEO_FEATURES = { + "observation.images.cam": { + "dtype": "video", + "shape": (64, 96, 3), + "names": ["height", "width", "channels"], + }, + "action": {"dtype": "float32", "shape": (2,), "names": ["a", "b"]}, +} +VIDEO_KEY = "observation.images.cam" + + +def _write_frames(imgs_dir: Path, num_frames: int = 4, height: int = 64, width: int = 96) -> None: + imgs_dir.mkdir(parents=True, exist_ok=True) + for i in range(num_frames): + arr = np.random.randint(0, 256, (height, width, 3), dtype=np.uint8) + write_image(arr, imgs_dir / f"frame-{i:06d}.png") + + +def _encode_video( + path: Path, num_frames: int = 4, fps: int = 30, cfg: VideoEncoderConfig | None = None +) -> Path: + imgs_dir = path.parent / f"imgs_{path.stem}" + _write_frames(imgs_dir, num_frames=num_frames) + encode_video_frames(imgs_dir, path, fps=fps, camera_encoder=cfg, overwrite=True) + return path + + +def _read_feature_info(dataset: LeRobotDataset) -> dict: + info = json.loads((dataset.root / INFO_PATH).read_text()) + return info["features"][VIDEO_KEY]["info"] + + +def _add_frames(dataset: LeRobotDataset, num_frames: int) -> None: + shape = dataset.meta.features[VIDEO_KEY]["shape"] + for _ in range(num_frames): + dataset.add_frame( + { + VIDEO_KEY: np.random.randint(0, 256, shape, dtype=np.uint8), + "action": np.zeros(2, dtype=np.float32), + "task": "test", + } + ) + + +class TestGetVideoInfo: + def test_returns_all_stream_fields(self): + info = get_video_info(TEST_ARTIFACTS_DIR / "clip_4frames.mp4") + + assert info["video.height"] == 64 + assert info["video.width"] == 96 + assert info["video.pix_fmt"] == "yuv420p" + assert info["video.fps"] == 30 + assert info["video.channels"] == 3 + assert info["video.is_depth_map"] is False + assert info["has_audio"] is False + assert "video.g" not in info + assert "video.crf" not in info + assert "video.preset" not in info + + @require_libsvtav1 + def test_merges_encoder_config_as_video_prefixed_entries(self): + cfg = VideoEncoderConfig(vcodec="libsvtav1", g=2, crf=30, preset=12) + + info = get_video_info(TEST_ARTIFACTS_DIR / "clip_4frames.mp4", camera_encoder=cfg) + + assert info["video.g"] == 2 + assert info["video.crf"] == 30 + assert info["video.preset"] == 12 + assert info["video.fast_decode"] == 0 + assert info["video.video_backend"] == "pyav" + assert info["video.extra_options"] == {} + + @require_libsvtav1 + def test_stream_derived_keys_take_precedence_over_config(self): + cfg = VideoEncoderConfig(vcodec="libsvtav1", pix_fmt="yuv420p") + + info = get_video_info(TEST_ARTIFACTS_DIR / "clip_4frames.mp4", camera_encoder=cfg) + + assert info["video.codec"] # populated from stream, not from config's vcodec + assert info["video.pix_fmt"] == "yuv420p" + + +class TestEncodeVideoFrames: + @require_libsvtav1 + def test_produces_readable_mp4(self, tmp_path): + video_path = _encode_video(tmp_path / "out.mp4") + + assert video_path.exists() + info = get_video_info(video_path) + assert info["video.height"] == 64 + assert info["video.width"] == 96 + + @require_libsvtav1 + def test_frame_count_and_duration_match_input(self, tmp_path): + num_frames = 10 + fps = 30 + video_path = _encode_video(tmp_path / "out.mp4", num_frames=num_frames, fps=fps) + + with av.open(str(video_path)) as container: + stream = container.streams.video[0] + actual_frames = sum(1 for _ in container.decode(stream)) + duration = ( + float(stream.duration * stream.time_base) + if stream.duration is not None + else float(container.duration / av.time_base) + ) + + assert actual_frames == num_frames + assert abs(duration - num_frames / fps) < 0.1 + + def test_overwrite_false_skips_existing_file(self, tmp_path): + imgs_dir = tmp_path / "imgs" + _write_frames(imgs_dir) + video_path = tmp_path / "out.mp4" + sentinel = b"pre-existing content" + video_path.write_bytes(sentinel) + + encode_video_frames(imgs_dir, video_path, fps=30, overwrite=False) + + assert video_path.read_bytes() == sentinel + + @require_libsvtav1 + def test_overwrite_true_replaces_existing_file(self, tmp_path): + imgs_dir = tmp_path / "imgs" + _write_frames(imgs_dir) + video_path = tmp_path / "out.mp4" + video_path.write_bytes(b"stale content") + + encode_video_frames(imgs_dir, video_path, fps=30, overwrite=True) + + info = get_video_info(video_path) + assert info["video.height"] == 64 + + @require_libsvtav1 + def test_custom_encoder_config_fields_stored_in_info(self, tmp_path): + """All stream-derived and encoder config fields are present after encoding.""" + cfg = VideoEncoderConfig(vcodec="libsvtav1", g=4, crf=25, preset=10) + video_path = _encode_video(tmp_path / "out.mp4", num_frames=4, fps=30, cfg=cfg) + + info = get_video_info(video_path, camera_encoder=cfg) + + # Stream-derived + assert info["video.height"] == 64 + assert info["video.width"] == 96 + assert info["video.channels"] == 3 + assert info["video.codec"] == "av1" + assert info["video.pix_fmt"] == "yuv420p" + assert info["video.fps"] == 30 + assert info["video.is_depth_map"] is False + assert info["has_audio"] is False + # Encoder config + assert info["video.g"] == 4 + assert info["video.crf"] == 25 + assert info["video.preset"] == 10 + assert info["video.fast_decode"] == 0 + assert info["video.video_backend"] == "pyav" + assert info["video.extra_options"] == {} + + +class TestConcatenateVideoFiles: + def test_two_clips_frame_count(self, tmp_path): + """Output frame count equals the sum of the two input frame counts.""" + out = tmp_path / "out.mp4" + concatenate_video_files( + [TEST_ARTIFACTS_DIR / "clip_6frames.mp4", TEST_ARTIFACTS_DIR / "clip_4frames.mp4"], out + ) + + with av.open(str(out)) as container: + total = sum(1 for _ in container.decode(video=0)) + assert total == 10 + + def test_three_clips_frame_count(self, tmp_path): + out = tmp_path / "out.mp4" + clip = TEST_ARTIFACTS_DIR / "clip_5frames.mp4" + concatenate_video_files([clip, clip, clip], out) + + with av.open(str(out)) as container: + total = sum(1 for _ in container.decode(video=0)) + assert total == 15 + + @require_libsvtav1 + def test_geometry_preserved(self, tmp_path): + """Output resolution, fps, codec and pixel format must match the inputs.""" + out = tmp_path / "out.mp4" + concatenate_video_files( + [TEST_ARTIFACTS_DIR / "clip_4frames.mp4", TEST_ARTIFACTS_DIR / "clip_4frames.mp4"], out + ) + + info = get_video_info(out) + assert info["video.height"] == 64 + assert info["video.width"] == 96 + assert info["video.fps"] == 30 + assert info["video.codec"] == "av1" + assert info["video.pix_fmt"] == "yuv420p" + + def test_compatibility_check_raises_on_different_codec(self, tmp_path): + with pytest.raises(ValueError): + concatenate_video_files( + [TEST_ARTIFACTS_DIR / "clip_4frames.mp4", TEST_ARTIFACTS_DIR / "clip_h264.mp4"], + tmp_path / "out.mp4", + compatibility_check=True, + ) + + def test_compatibility_check_raises_on_different_resolution(self, tmp_path): + with pytest.raises(ValueError): + concatenate_video_files( + [TEST_ARTIFACTS_DIR / "clip_4frames.mp4", TEST_ARTIFACTS_DIR / "clip_32x48.mp4"], + tmp_path / "out.mp4", + compatibility_check=True, + ) + + +class TestEncoderConfigPersistence: + """Encoder config must be stored as ``video.`` entries in + ``info["features"][key]["info"]`` when the first episode is saved. + """ + + @require_libsvtav1 + def test_first_episode_save_persists_encoder_config(self, tmp_path, empty_lerobot_dataset_factory): + cfg = VideoEncoderConfig(vcodec="libsvtav1", g=2, crf=30, preset=12) + dataset = empty_lerobot_dataset_factory( + root=tmp_path / "ds", features=VIDEO_FEATURES, use_videos=True, camera_encoder=cfg + ) + + _add_frames(dataset, num_frames=4) + dataset.save_episode() + dataset.finalize() + + info = _read_feature_info(dataset) + + assert info["video.height"] == 64 + assert info["video.width"] == 96 + assert info["video.fps"] == 30 + assert info["video.g"] == 2 + assert info["video.crf"] == 30 + assert info["video.preset"] == 12 + assert info["video.fast_decode"] == 0 + assert info["video.video_backend"] == "pyav" + assert info["video.extra_options"] == {} + + @require_libsvtav1 + def test_second_episode_does_not_overwrite_encoder_fields(self, tmp_path, empty_lerobot_dataset_factory): + cfg = VideoEncoderConfig(vcodec="libsvtav1", g=2, crf=30, preset=12) + dataset = empty_lerobot_dataset_factory( + root=tmp_path / "ds", features=VIDEO_FEATURES, use_videos=True, camera_encoder=cfg + ) + + _add_frames(dataset, num_frames=4) + dataset.save_episode() + first_info = dict(_read_feature_info(dataset)) + + _add_frames(dataset, num_frames=4) + dataset.save_episode() + dataset.finalize() + + assert _read_feature_info(dataset) == first_info + + +class TestFromVideoInfo: + """``VideoEncoderConfig.from_video_info`` reconstructs an encoder config + from the ``video.*`` keys persisted in a dataset's ``info.json``. + """ + + @require_libsvtav1 + def test_reconstructs_from_dummy_video_info(self): + cfg = VideoEncoderConfig.from_video_info(DUMMY_VIDEO_INFO) + + # Canonical stream codec ``"av1"`` is aliased to the encoder name. + assert cfg.vcodec == "libsvtav1" + assert cfg.pix_fmt == DUMMY_VIDEO_INFO["video.pix_fmt"] + assert cfg.g == DUMMY_VIDEO_INFO["video.g"] + assert cfg.crf == DUMMY_VIDEO_INFO["video.crf"] + assert cfg.preset == DUMMY_VIDEO_INFO["video.preset"] + assert cfg.fast_decode == DUMMY_VIDEO_INFO["video.fast_decode"] + assert cfg.video_backend == DUMMY_VIDEO_INFO["video.video_backend"] + # ``{}`` placeholder (typical after a merge with disagreeing sources) + # must not leak into the reconstructed config. + assert cfg.extra_options == VideoEncoderConfig().extra_options diff --git a/tests/fixtures/constants.py b/tests/fixtures/constants.py index 35d8776ce..4d578b503 100644 --- a/tests/fixtures/constants.py +++ b/tests/fixtures/constants.py @@ -28,17 +28,23 @@ DUMMY_MOTOR_FEATURES = { "names": ["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll", "gripper"], }, } -DUMMY_CAMERA_FEATURES = { - "laptop": {"shape": (64, 96, 3), "names": ["height", "width", "channels"], "info": None}, - "phone": {"shape": (64, 96, 3), "names": ["height", "width", "channels"], "info": None}, -} DEFAULT_FPS = 30 DUMMY_VIDEO_INFO = { "video.fps": DEFAULT_FPS, "video.codec": "av1", "video.pix_fmt": "yuv420p", + "video.video_backend": "pyav", + "video.extra_options": {}, + "video.g": 2, + "video.crf": 30, + "video.preset": 12, + "video.fast_decode": 0, "video.is_depth_map": False, "has_audio": False, } +DUMMY_CAMERA_FEATURES = { + "laptop": {"shape": (64, 96, 3), "names": ["height", "width", "channels"], "info": DUMMY_VIDEO_INFO}, + "phone": {"shape": (64, 96, 3), "names": ["height", "width", "channels"], "info": DUMMY_VIDEO_INFO}, +} DUMMY_CHW = (3, 96, 128) DUMMY_HWC = (96, 128, 3) diff --git a/tests/fixtures/dataset_factories.py b/tests/fixtures/dataset_factories.py index 48128a8d0..a6e349778 100644 --- a/tests/fixtures/dataset_factories.py +++ b/tests/fixtures/dataset_factories.py @@ -46,7 +46,6 @@ from tests.fixtures.constants import ( DUMMY_MOTOR_FEATURES, DUMMY_REPO_ID, DUMMY_ROBOT_TYPE, - DUMMY_VIDEO_INFO, ) @@ -134,9 +133,7 @@ def features_factory(): use_videos: bool = True, ) -> dict: if use_videos: - camera_ft = { - key: {"dtype": "video", **ft, **DUMMY_VIDEO_INFO} for key, ft in camera_features.items() - } + camera_ft = {key: {"dtype": "video", **ft} for key, ft in camera_features.items()} else: camera_ft = {key: {"dtype": "image", **ft} for key, ft in camera_features.items()} return { From 01dcb4c29222bc9f2388cebf87f0e79965a9508b Mon Sep 17 00:00:00 2001 From: Haoming Song Date: Fri, 15 May 2026 17:37:05 +0800 Subject: [PATCH 18/18] fix(pi05): update pi05 with transformers v5.4.0 interface (#3603) --- src/lerobot/policies/pi05/modeling_pi05.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lerobot/policies/pi05/modeling_pi05.py b/src/lerobot/policies/pi05/modeling_pi05.py index bb206d608..bdaf01f2c 100644 --- a/src/lerobot/policies/pi05/modeling_pi05.py +++ b/src/lerobot/policies/pi05/modeling_pi05.py @@ -441,13 +441,13 @@ class PaliGemmaWithExpertModel( if image.dtype != torch.float32: image = image.to(torch.float32) image_outputs = self.paligemma.model.get_image_features(image) - features = image_outputs.pooler_output * self.paligemma.config.text_config.hidden_size**0.5 + features = image_outputs.pooler_output if features.dtype != out_dtype: features = features.to(out_dtype) return features def embed_language_tokens(self, tokens: torch.Tensor): - return self.paligemma.model.language_model.embed_tokens(tokens) + return self.paligemma.model.language_model.get_input_embeddings()(tokens) def forward( self, @@ -662,8 +662,7 @@ class PI05Pytorch(nn.Module): # see openpi `PI0Pytorch` # Process language tokens def lang_embed_func(tokens): lang_emb = self.paligemma_with_expert.embed_language_tokens(tokens) - lang_emb_dim = lang_emb.shape[-1] - return lang_emb * math.sqrt(lang_emb_dim) + return lang_emb lang_emb = self._apply_checkpoint(lang_embed_func, tokens) embs.append(lang_emb)