From e31b91aa6265e9c9a1aceb267ea48d7f73e97b04 Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:39:11 -0800 Subject: [PATCH 01/13] bump version --- README.md | 3 +- pyproject.toml | 2 +- uv.lock | 633 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 635 insertions(+), 3 deletions(-) create mode 100644 uv.lock diff --git a/README.md b/README.md index 74207ff..2c5597a 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ ![processed-image](demo.png) ## Installation and Setup -The program ~~is~~ *was* intended to work on a Windows OS with Python versions 3.10-3.12. -However, I can only test this on Ubuntu now. +The program is intended to work on a Windows OS with Python versions 3.10-3.12. Open a Python virtual environment and install the Git repository. For example: ```console diff --git a/pyproject.toml b/pyproject.toml index c11917f..9966654 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "kksubs" -version = "3.1.0" +version = "3.2.0-dev" requires-python = ">=3.10" dependencies = [ "natsort", diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..4013641 --- /dev/null +++ b/uv.lock @@ -0,0 +1,633 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/2d/63e37369c8e81a643afe54f76073b020f7b97ddbe698c5c944b51b0a2bc5/coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b", size = 218842, upload-time = "2026-01-25T12:57:15.3Z" }, + { url = "https://files.pythonhosted.org/packages/57/06/86ce882a8d58cbcb3030e298788988e618da35420d16a8c66dac34f138d0/coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2", size = 219360, upload-time = "2026-01-25T12:57:17.572Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/70b0eb1ee19ca4ef559c559054c59e5b2ae4ec9af61398670189e5d276e9/coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896", size = 246123, upload-time = "2026-01-25T12:57:19.087Z" }, + { url = "https://files.pythonhosted.org/packages/35/fb/05b9830c2e8275ebc031e0019387cda99113e62bb500ab328bb72578183b/coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c", size = 247930, upload-time = "2026-01-25T12:57:20.929Z" }, + { url = "https://files.pythonhosted.org/packages/81/aa/3f37858ca2eed4f09b10ca3c6ddc9041be0a475626cd7fd2712f4a2d526f/coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc", size = 249804, upload-time = "2026-01-25T12:57:22.904Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/c904f40c56e60a2d9678a5ee8df3d906d297d15fb8bec5756c3b0a67e2df/coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5", size = 246815, upload-time = "2026-01-25T12:57:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/41/91/ddc1c5394ca7fd086342486440bfdd6b9e9bda512bf774599c7c7a0081e0/coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31", size = 247843, upload-time = "2026-01-25T12:57:26.544Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/cdff8f4cd33697883c224ea8e003e9c77c0f1a837dc41d95a94dd26aad67/coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad", size = 245850, upload-time = "2026-01-25T12:57:28.507Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/e837febb7866bf2553ab53dd62ed52f9bb36d60c7e017c55376ad21fbb05/coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f", size = 246116, upload-time = "2026-01-25T12:57:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/09/b1/4a3f935d7df154df02ff4f71af8d61298d713a7ba305d050ae475bfbdde2/coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8", size = 246720, upload-time = "2026-01-25T12:57:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/538a6fd44c515f1c5197a3f078094cbaf2ce9f945df5b44e29d95c864bff/coverage-7.13.2-cp310-cp310-win32.whl", hash = "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c", size = 221465, upload-time = "2026-01-25T12:57:33.511Z" }, + { url = "https://files.pythonhosted.org/packages/5e/09/4b63a024295f326ec1a40ec8def27799300ce8775b1cbf0d33b1790605c4/coverage-7.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99", size = 222397, upload-time = "2026-01-25T12:57:34.927Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, + { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "kksubs" +version = "3.2.0.dev0" +source = { editable = "." } +dependencies = [ + { name = "natsort" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "opencv-python" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyyaml" }, +] + +[package.optional-dependencies] +test = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "natsort" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "pytest-cov", marker = "extra == 'test'" }, + { name = "pyyaml" }, +] +provides-extras = ["test"] + +[[package]] +name = "natsort" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.13.0.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d7/133d5756aef78090f4d8dd4895793aed24942dec6064a15375cfac9175fc/opencv_python-4.13.0.90-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:58803f8b05b51d8a785e2306d83b44173b32536f980342f3bc76d8c122b5938d", size = 46020278, upload-time = "2026-01-18T08:57:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/7b/65/3b8cdbe13fa2436695d00e1d8c1ddf5edb4050a93436f34ed867233d1960/opencv_python-4.13.0.90-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:a5354e8b161409fce7710ba4c1cfe88b7bb460d97f705dc4e714a1636616f87d", size = 32568376, upload-time = "2026-01-18T08:58:47.19Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/e4d7c165e678563f49505d3d2811fcc16011e929cd00bc4b0070c7ee82b0/opencv_python-4.13.0.90-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d557cbf0c7818081c9acf56585b68e781af4f00638971f75eaa3de70904a6314", size = 47685110, upload-time = "2026-01-18T08:59:58.045Z" }, + { url = "https://files.pythonhosted.org/packages/cf/02/d9b73dbce28712204e85ae4c1e179505e9a771f95b33743a97e170caedde/opencv_python-4.13.0.90-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9911581e37b24169e4842069ff01d6645ea2bc4af7e10a022d9ebe340fd035ec", size = 70460479, upload-time = "2026-01-18T09:01:16.377Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1c/87fa71968beb71481ed359e21772061ceff7c9b45a61b3e7daa71e5b0b66/opencv_python-4.13.0.90-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1150b8f1947761b848bbfa9c96ceba8877743ffef157c08a04af6f7717ddd709", size = 46707819, upload-time = "2026-01-18T09:02:48.049Z" }, + { url = "https://files.pythonhosted.org/packages/af/16/915a94e5b537c328fa3e96b769c7d4eed3b67d1be978e0af658a3d3faed8/opencv_python-4.13.0.90-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:d6716f16149b04eea52f953b8ca983d60dd9cd4872c1fd5113f6e2fcebb90e93", size = 72926629, upload-time = "2026-01-18T09:04:29.23Z" }, + { url = "https://files.pythonhosted.org/packages/bf/84/9c63c84be013943dd4c5fff36157f1ec0ec894b69a2fc3026fd4e3c9280a/opencv_python-4.13.0.90-cp37-abi3-win32.whl", hash = "sha256:458a00f2ba47a877eca385be3e7bcc45e6d30a4361d107ce73c1800f516dab09", size = 30932151, upload-time = "2026-01-18T09:05:22.181Z" }, + { url = "https://files.pythonhosted.org/packages/13/de/291cbb17f44242ed6bfd3450fc2535d6bd298115c0ccd6f01cd51d4a11d7/opencv_python-4.13.0.90-cp37-abi3-win_amd64.whl", hash = "sha256:526bde4c33a86808a751e2bb57bf4921beb49794621810971926c472897f6433", size = 40211706, upload-time = "2026-01-18T09:06:06.749Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, + { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" }, + { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, + { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, + { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, + { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, + { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, + { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, + { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, + { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, + { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, + { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, + { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, + { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, + { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] From 17c69b1ded0af2ee5e4e9f99f80ad053b4789ced Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:43:36 -0800 Subject: [PATCH 02/13] add ruff --- pyproject.toml | 3 ++- uv.lock | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9966654..a06b43d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "packaging", "pillow", "pyyaml", + "ruff>=0.14.14", ] [project.optional-dependencies] @@ -39,4 +40,4 @@ testpaths = ["test"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] -addopts = "-v --tb=short" \ No newline at end of file +addopts = "-v --tb=short" diff --git a/uv.lock b/uv.lock index 4013641..750c355 100644 --- a/uv.lock +++ b/uv.lock @@ -152,6 +152,7 @@ dependencies = [ { name = "packaging" }, { name = "pillow" }, { name = "pyyaml" }, + { name = "ruff" }, ] [package.optional-dependencies] @@ -170,6 +171,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'test'" }, { name = "pytest-cov", marker = "extra == 'test'" }, { name = "pyyaml" }, + { name = "ruff", specifier = ">=0.14.14" }, ] provides-extras = ["test"] @@ -569,6 +571,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "ruff" +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, +] + [[package]] name = "tomli" version = "2.4.0" From d0a3e4bf200ea804e952fe258df52e1e59a8e0f7 Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:52:35 -0800 Subject: [PATCH 03/13] pass ruff checks --- src/common/utils/file.py | 4 +-- src/common/watcher/abstract.py | 4 +-- src/kkp/cmd/project.py | 12 +++---- src/kkp/controller/project.py | 38 ++++++++++---------- src/kkp/service/studio_project.py | 28 +++++++-------- src/kksubs/__init__.py | 2 -- src/kksubs/cmd/subtitle.py | 4 +-- src/kksubs/data/subtitle/style.py | 7 +++- src/kksubs/data/subtitle/style_attributes.py | 6 ++-- src/kksubs/data/subtitle/style_row_enum.py | 3 +- src/kksubs/data/subtitle/subtitle.py | 4 +-- src/kksubs/service/extraction/style.py | 2 +- src/kksubs/service/extraction/subtitle.py | 7 ++-- src/kksubs/service/file.py | 1 - src/kksubs/service/processor/apply_text.py | 2 +- src/kksubs/service/sub_project.py | 8 ++--- src/kksubs/service/subtitle.py | 3 +- src/koi/cli.py | 10 +++--- test/test_common/test_file_operations.py | 6 +++- test/test_kkp/test_projects.py | 2 -- test/test_kksubs/test_serialization.py | 6 ++-- 21 files changed, 83 insertions(+), 76 deletions(-) diff --git a/src/common/utils/file.py b/src/common/utils/file.py index 62de532..abcd45b 100644 --- a/src/common/utils/file.py +++ b/src/common/utils/file.py @@ -241,7 +241,7 @@ def _sync_unidirectional(source, destination, filename_filter:List[str]=None): in_both = source_paths.intersection(dest_paths) # conflict resolution. (see modified/unmodified) modified = set(filter(lambda path: source_bucket.files[path] > dest_bucket.files[path], in_both)) - unmodified = in_both.difference(modified) + in_both.difference(modified) delete_from_dest = dest_paths.difference(source_paths) add_to_dest = source_paths.difference(dest_paths) @@ -343,7 +343,7 @@ def _get_bidirectional_deltas( deleted, ])) - total_num_files = total_changes + len(unmodified) + total_changes + len(unmodified) return SyncDeltas( unmodified=unmodified, diff --git a/src/common/watcher/abstract.py b/src/common/watcher/abstract.py index 50bc790..f2dc7df 100644 --- a/src/common/watcher/abstract.py +++ b/src/common/watcher/abstract.py @@ -2,7 +2,7 @@ import logging import time import traceback -from common.exceptions import * +from common.exceptions import RetryWatcherPrompt logger = logging.getLogger(__name__) @@ -12,7 +12,7 @@ def __init__(self): self.sleep_time = 1 def setup_watch(self): - logger.info(f'Setting up watcher.') + logger.info('Setting up watcher.') @abstractmethod def event_trigger_action(self): diff --git a/src/kkp/cmd/project.py b/src/kkp/cmd/project.py index 7229ab6..20c70de 100644 --- a/src/kkp/cmd/project.py +++ b/src/kkp/cmd/project.py @@ -30,9 +30,9 @@ def command_line(): compose_parser = subparsers.add_parser('compose', help='Compose subtitles once.') compose_parser.add_argument('--incremental-update', action='store_true', help='Allow incremental update.') - activate_parser = subparsers.add_parser('activate', help='Compose subtitles continuously.') + subparsers.add_parser('activate', help='Compose subtitles continuously.') - clear_parser = subparsers.add_parser('clear', help='Clear subtitle outputs.') + subparsers.add_parser('clear', help='Clear subtitle outputs.') create_parser = subparsers.add_parser('create', help='Create a project.') create_parser.add_argument('project_name', type=str, help='Name of project to create.') @@ -47,13 +47,13 @@ def command_line(): delete_parser = subparsers.add_parser('delete', help='Delete a project or multiple projects') delete_parser.add_argument('project_name', type=str, help='Name of project to delete.') - sync_parser = subparsers.add_parser('sync', help='Sync current project with library.') + subparsers.add_parser('sync', help='Sync current project with library.') - studio_parser = subparsers.add_parser('studio', help='Open Koikatsu Charastudio application.') + subparsers.add_parser('studio', help='Open Koikatsu Charastudio application.') - library_parser = subparsers.add_parser('library', help='Open library folder.') + subparsers.add_parser('library', help='Open library folder.') - game_parser = subparsers.add_parser('game', help='Open Koikatsu game application.') + subparsers.add_parser('game', help='Open Koikatsu game application.') game_folder_parser = subparsers.add_parser('game-folder', help='Open Koikatsu Party game folder.') game_folder_parser.add_argument('-s', '--shortcut', type=str, help='Shortcut to open a subfolder.') diff --git a/src/kkp/controller/project.py b/src/kkp/controller/project.py index 1f35563..09b20aa 100644 --- a/src/kkp/controller/project.py +++ b/src/kkp/controller/project.py @@ -1,7 +1,8 @@ import logging import os +import shutil from pathlib import Path -from typing import List, Optional +from typing import Dict, List, Optional import yaml from common.exceptions import InvalidProjectException import subprocess @@ -16,9 +17,8 @@ from kkp.view.project import ProjectView from kkp.watcher.project import ProjectWatcher from common.utils.coalesce import coalesce -from common.utils.file import * -from common.utils.decorators import * -from common.utils.application import * +from common.utils.decorators import deprecated, spacing +from common.utils.application import get_config_path, get_application_root from common.exceptions import NotConfiguredException # config information @@ -458,7 +458,7 @@ def info(self, show_recent_projects:bool=False): recent_projects = list() try: self.set_quick_access(projects+recent_projects) - except: + except Exception: raise Exception((projects, recent_projects)) current_project = self.current_project @@ -651,7 +651,7 @@ def checkout(self, project_name:str, new_branch:bool=False, compose:bool=True) - print(f'Loading project {project_name}') self.sync(compose=compose) self.studio_project_service.correct_scene_order() - print(f'Checkout successful.') + print('Checkout successful.') return True def _create(self, project_name:str, compose:bool=True): @@ -708,13 +708,13 @@ def set_quick_access(self, project_list:List[str]): self.quick_access = project_list def list_recent_projects(self, limit:Optional[int]=None, start_index:Optional[int]=None, update_quick_access:bool=True) -> List[str]: - logger.info(f'Listing recent projects.') + logger.info('Listing recent projects.') if start_index is None: start_index = 0 recent_projects = self.get_recent_projects() if not recent_projects: - print(f'No recent projects found.') + print('No recent projects found.') return list() num_projects = len(recent_projects) @@ -801,7 +801,7 @@ def rename(self, new_project_id: str): print(f"Successfully renamed project {self.current_project} to {new_project_id}.") return else: - print(f"Failed to rename project.") + print("Failed to rename project.") def delete(self, project_name:str, safe:bool=True): """ @@ -843,18 +843,18 @@ def _sync_workspace(self): def sync(self, compose:bool=True): if self.current_project is None: - logger.error(f'No assigned project to sync with.') + logger.error('No assigned project to sync with.') return self._sync_studio() - logger.info(f'Synced studio.') + logger.info('Synced studio.') self._sync_workspace() - logger.info(f'Synced subtitle project.') + logger.info('Synced subtitle project.') self._pull_captures() - logger.info(f'Retrieved captures.') + logger.info('Retrieved captures.') if compose: - logger.info(f'Applying subtitles...') + logger.info('Applying subtitles...') self.compose(incremental_update=True) - logger.info(f'Applied subtitles.') + logger.info('Applied subtitles.') def save_current_project(self): self.sync(compose=True) @@ -888,7 +888,7 @@ def open_studio(self): if studio_exe_path is None: raise FileNotFoundError("CharaStudio executable not found") - print(f'Launching Studio; please wait...') + print('Launching Studio; please wait...') subprocess.Popen(studio_exe_path) return @@ -900,7 +900,7 @@ def open_game(self): if game_exe_path is None: raise FileNotFoundError("Koikatsu Party executable not found") - print(f'Launching Koikatsu Party; please wait...') + print('Launching Koikatsu Party; please wait...') subprocess.Popen(game_exe_path) return @@ -1004,7 +1004,7 @@ def merge_project(self, project_name:str): project_names = self.studio_project_service.list_projects(pattern=project_name) if not project_names: - print(f'No projects found.') + print('No projects found.') return if len(project_names) == 1: @@ -1018,7 +1018,7 @@ def merge_project(self, project_name:str): for p in project_names: print(f'- {p}') print('You are about to merge the above projects into your current project. Files of the same name will be overridden.') - confirm = input(f'Proceed? (Y) ') == 'Y' + confirm = input('Proceed? (Y) ') == 'Y' if not confirm: return diff --git a/src/kkp/service/studio_project.py b/src/kkp/service/studio_project.py index 8c4e740..5e575af 100644 --- a/src/kkp/service/studio_project.py +++ b/src/kkp/service/studio_project.py @@ -5,9 +5,7 @@ import re import shutil import fnmatch -import tempfile import datetime -from natsort import natsorted from common.data.file import Bucket from common.exceptions import InvalidProjectException @@ -39,17 +37,19 @@ def __init__( self.subtitle_file_names = subtitle_file_names self.file_service = FileService() - def get_sync_state(self, project_path) -> Dict: - sync_state = dict() - for file in self.studio_file_names: - file_path = os.path.join(project_path, file) - if not os.path.exists(file_path): - continue - if os.path.isfile(file_path): - sync_state[file_path] = os.path.getmtime(file_path) - if os.path.isdir(file_path): - sync_state[file_path] = Bucket(file_path).state() - return sync_state + # def get_sync_state(self, project_path) -> Dict: + # # NOTE: This method is commented out due to redefinition with get_sync_state() at line ~191 + # # The parameterless version is currently in use. + # sync_state = dict() + # for file in self.studio_file_names: + # file_path = os.path.join(project_path, file) + # if not os.path.exists(file_path): + # continue + # if os.path.isfile(file_path): + # sync_state[file_path] = os.path.getmtime(file_path) + # if os.path.isdir(file_path): + # sync_state[file_path] = Bucket(file_path).state() + # return sync_state def to_project_path(self, project_name:str) -> str: """ @@ -311,7 +311,7 @@ def delete_projects(self, project_names, safe=True) -> bool: for name in project_names: print(f"- {name}") print("-----") - confirmation = input(f"Confirm (y/n): ") + confirmation = input("Confirm (y/n): ") if confirmation!= "y": return False diff --git a/src/kksubs/__init__.py b/src/kksubs/__init__.py index 061b3b0..6341afe 100644 --- a/src/kksubs/__init__.py +++ b/src/kksubs/__init__.py @@ -1,5 +1,3 @@ -from typing import Dict, List - from kksubs.controller.subtitle import SubtitleController # basic functions diff --git a/src/kksubs/cmd/subtitle.py b/src/kksubs/cmd/subtitle.py index 508f66b..d7b6c09 100644 --- a/src/kksubs/cmd/subtitle.py +++ b/src/kksubs/cmd/subtitle.py @@ -23,9 +23,9 @@ def command_line(): subparsers = parser.add_subparsers(dest='command') - init_parser = subparsers.add_parser('init', help='Initialize a project.') + subparsers.add_parser('init', help='Initialize a project.') - rename_parser = subparsers.add_parser('rename', help='Rename images in project.') + subparsers.add_parser('rename', help='Rename images in project.') activate_parser = subparsers.add_parser('activate', help='Compose subtitles continuously.') diff --git a/src/kksubs/data/subtitle/style.py b/src/kksubs/data/subtitle/style.py index 053651d..0094653 100644 --- a/src/kksubs/data/subtitle/style.py +++ b/src/kksubs/data/subtitle/style.py @@ -2,7 +2,12 @@ from copy import deepcopy from typing import List -from kksubs.data.subtitle.style_attributes import * +from common.data.representable import RepresentableData +from common.utils.coalesce import coalesce +from kksubs.data.subtitle.style_attributes import ( + Asset, Background, BaseData, BoxData, Brightness, Gaussian, + Mask, Motion, OutlineData, OutlineData1, TextData +) class Style(BaseData): field_name = "style" diff --git a/src/kksubs/data/subtitle/style_attributes.py b/src/kksubs/data/subtitle/style_attributes.py index e9445d0..79127a2 100644 --- a/src/kksubs/data/subtitle/style_attributes.py +++ b/src/kksubs/data/subtitle/style_attributes.py @@ -1,6 +1,6 @@ -from kksubs.data.abstract import * -from common.utils.coalesce import * -from kksubs.utils.sanitizers import * +from kksubs.data.abstract import BaseData +from common.utils.coalesce import coalesce +from kksubs.utils.sanitizers import to_float, to_integer, to_rgb_color, to_string, to_validated_value, to_xy_coords class TextData(BaseData): field_name = "text_data" diff --git a/src/kksubs/data/subtitle/style_row_enum.py b/src/kksubs/data/subtitle/style_row_enum.py index 3ba797a..128eab1 100644 --- a/src/kksubs/data/subtitle/style_row_enum.py +++ b/src/kksubs/data/subtitle/style_row_enum.py @@ -1,7 +1,8 @@ from enum import Enum from typing import List -from kksubs.data.subtitle.style import * +from kksubs.data.subtitle.style import Style, StyleRow +from kksubs.data.subtitle.style_attributes import BoxData grid4_complete:List[Style] = list() for i in range(4): diff --git a/src/kksubs/data/subtitle/subtitle.py b/src/kksubs/data/subtitle/subtitle.py index c5a25fc..dbacf32 100644 --- a/src/kksubs/data/subtitle/subtitle.py +++ b/src/kksubs/data/subtitle/subtitle.py @@ -2,9 +2,7 @@ import os from typing import Dict, List -from kksubs.utils.sanitizers import * -from kksubs.data.abstract import * -from kksubs.data.subtitle.style_attributes import * +from kksubs.data.abstract import RepresentableData from kksubs.data.subtitle.style import Style logger = logging.getLogger(__name__) diff --git a/src/kksubs/service/extraction/style.py b/src/kksubs/service/extraction/style.py index 5cb6f73..b873c96 100644 --- a/src/kksubs/service/extraction/style.py +++ b/src/kksubs/service/extraction/style.py @@ -2,7 +2,7 @@ import re from typing import List, Dict, Union -from kksubs.data.subtitle.style import * +from kksubs.data.subtitle.style import ContextLayer, Layer, Style, StyleMatrix, StyleRow from kksubs.data.subtitle.style_row_enum import STYLE_ROW_ENUM logger = logging.getLogger(__name__) diff --git a/src/kksubs/service/extraction/subtitle.py b/src/kksubs/service/extraction/subtitle.py index 4644d30..8bdbf02 100644 --- a/src/kksubs/service/extraction/subtitle.py +++ b/src/kksubs/service/extraction/subtitle.py @@ -2,7 +2,10 @@ from typing import Dict, List, Set from copy import deepcopy -from kksubs.data.subtitle.style_attributes import * +from kksubs.data.subtitle.style_attributes import ( + Asset, Background, BaseData, BoxData, Brightness, Gaussian, + Mask, Motion, OutlineData, OutlineData1, TextData +) from kksubs.data.subtitle.style import Style from kksubs.data.subtitle.subtitle import Subtitle, SubtitleGroup # from kksubs.data.subtitle.subtitle import Background, BaseData, BoxData, Brightness, Gaussian, Mask, Motion, OutlineData, OutlineData1, Style, Subtitle, SubtitleGroup, TextData @@ -163,7 +166,7 @@ def extract_subtitle_groups( draft_id:str, draft_body:str, styles:Dict[str, Style], image_dir:str, output_dir:str, prefix:str=None ) -> Dict[str, List[SubtitleGroup]]: # extract subtitle groups from draft - logger.info(f"Extracting subtitle groups.") + logger.info("Extracting subtitle groups.") # subtitles = dict() subtitle_groups_by_image_id:Dict[str, List[SubtitleGroup]] = dict() diff --git a/src/kksubs/service/file.py b/src/kksubs/service/file.py index 33c8d97..e623ac4 100644 --- a/src/kksubs/service/file.py +++ b/src/kksubs/service/file.py @@ -1,6 +1,5 @@ import os from typing import Dict, List, Union -import yaml from common.data.file import Bucket from common.utils.file import transfer, sync_bidirectional, sync_unidirectional, save_most_recent_path diff --git a/src/kksubs/service/processor/apply_text.py b/src/kksubs/service/processor/apply_text.py index 17749f1..ec288a5 100644 --- a/src/kksubs/service/processor/apply_text.py +++ b/src/kksubs/service/processor/apply_text.py @@ -39,7 +39,7 @@ def create_text_layer( text_dimensions = [_get_text_dimensions(line, font, default_text_width=default_text_width, default_text_height=default_text_height) for line in wrapped_text] text_widths = list(map(lambda dim:dim[0], text_dimensions)) - max_text_width = max(text_widths) + max(text_widths) num_lines = len(wrapped_text) sum_text_height = num_lines * default_text_height diff --git a/src/kksubs/service/sub_project.py b/src/kksubs/service/sub_project.py index e0ddcaf..14b19d6 100644 --- a/src/kksubs/service/sub_project.py +++ b/src/kksubs/service/sub_project.py @@ -13,7 +13,7 @@ from kksubs.data.subtitle.style import Style from kksubs.data.subtitle.subtitle import SubtitleGroup -from common.exceptions import * +from common.exceptions import InvalidProjectException, RetryWatcherPrompt from kksubs.service.extraction.subtitle import extract_subtitle_groups from kksubs.service.extraction.style import extract_styles @@ -286,10 +286,10 @@ def incremental_update(self, draft_name:str, subtitle_group_by_image_id:Dict[str # state_path_v2 = self.get_state_path_v2(draft_name) if not os.path.exists(self.metadata_directory): - logger.info(f"Creating additional data directory.") + logger.info("Creating additional data directory.") os.mkdir(self.metadata_directory) if not os.path.exists(self.state_directory): - logger.info(f"Creating states directory.") + logger.info("Creating states directory.") os.mkdir(self.state_directory) if not os.path.exists(state_path): @@ -320,7 +320,7 @@ def incremental_update(self, draft_name:str, subtitle_group_by_image_id:Dict[str d4 = prev_image_ids.intersection(out_image_ids).difference(curr_image_ids) # delete output image. d5 = curr_image_ids.difference(prev_image_ids).difference(out_image_ids) # add subtitle. - d6 = prev_image_ids.difference(curr_image_ids).difference(out_image_ids) # do nothing. + prev_image_ids.difference(curr_image_ids).difference(out_image_ids) # do nothing. d7 = out_image_ids.difference(curr_image_ids).difference(prev_image_ids) # delete output image. # print(d1, d2, d3, d4, d5, d6, d7) diff --git a/src/kksubs/service/subtitle.py b/src/kksubs/service/subtitle.py index 37a64d5..c3aac2d 100644 --- a/src/kksubs/service/subtitle.py +++ b/src/kksubs/service/subtitle.py @@ -3,7 +3,8 @@ from PIL import Image, ImageFont, ImageFilter, ImageEnhance import importlib.resources -from kksubs.data.subtitle.style_attributes import * +from common.utils.coalesce import coalesce +from kksubs.data.subtitle.style_attributes import OutlineData from kksubs.data.subtitle.subtitle import Subtitle # from kksubs.data.subtitle.subtitle import OutlineData, Subtitle from kksubs.service.processor.motion_blur import apply_motion_blur diff --git a/src/koi/cli.py b/src/koi/cli.py index 1a6b15b..ed34979 100644 --- a/src/koi/cli.py +++ b/src/koi/cli.py @@ -24,10 +24,10 @@ def cli(): subparsers = parser.add_subparsers(dest='command', required=True) # Version - version_parser = subparsers.add_parser("version", help="Get version.") + subparsers.add_parser("version", help="Get version.") # Configure - configure_parser = subparsers.add_parser("configure", help="Configure the application.") + subparsers.add_parser("configure", help="Configure the application.") # Archive utilities archive_parser = subparsers.add_parser("archive", help="Archive commands") @@ -56,17 +56,17 @@ def cli(): run_parser.add_argument("--forever", action="store_true", help="Run indefinitely") # Sync - sync_parser = subparsers.add_parser("sync", help="Sync files with remote archive") + subparsers.add_parser("sync", help="Sync files with remote archive") # Export - export_parser = subparsers.add_parser("export", help="Export into a gallery") + subparsers.add_parser("export", help="Export into a gallery") # Info info_parser = subparsers.add_parser("info", help="Show kksubs info") info_parser.add_argument("-r", "--recent", action='store_true', help="Show recent Projects.") # Show - show_parser = subparsers.add_parser("show", help="Show subtitled images") + subparsers.add_parser("show", help="Show subtitled images") args = parser.parse_args() command = args.command diff --git a/test/test_common/test_file_operations.py b/test/test_common/test_file_operations.py index 18be3ac..70612d2 100644 --- a/test/test_common/test_file_operations.py +++ b/test/test_common/test_file_operations.py @@ -1,3 +1,4 @@ +import logging import os import tempfile from pathlib import Path @@ -5,7 +6,10 @@ import pytest import shutil -from common.utils.file import * +from common.data.file import Bucket +from common.utils.file import sync_bidirectional, sync_unidirectional, transfer + +logger = logging.getLogger(__name__) def create_files_in_directory(parent_directory, files: List[str]): diff --git a/test/test_kkp/test_projects.py b/test/test_kkp/test_projects.py index 27733a3..155c3e0 100644 --- a/test/test_kkp/test_projects.py +++ b/test/test_kkp/test_projects.py @@ -18,10 +18,8 @@ import tempfile import os -from pathlib import Path import pytest -from common.exceptions import InvalidProjectException from kkp.controller.project import ProjectController diff --git a/test/test_kksubs/test_serialization.py b/test/test_kksubs/test_serialization.py index ff18559..b0294ae 100644 --- a/test/test_kksubs/test_serialization.py +++ b/test/test_kksubs/test_serialization.py @@ -1,9 +1,9 @@ from typing import Type from common.data.representable import RepresentableData -from kksubs.data.subtitle.style_attributes import * -from kksubs.data.subtitle.style import * -from kksubs.data.subtitle.subtitle import * +from kksubs.data.subtitle.style_attributes import Asset, Background, BoxData, Brightness, Gaussian, Motion, OutlineData, TextData +from kksubs.data.subtitle.style import Style +from kksubs.data.subtitle.subtitle import Subtitle, SubtitleGroup def assert_representations_equal(representable_data: RepresentableData, representation_class: Type[RepresentableData]): From 3d62e56b8a0abbc8d990ef2c08f932a32dadd445 Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:59:35 -0800 Subject: [PATCH 04/13] modernize with test/dev --- pyproject.toml | 8 ++++---- {test => tests}/__init__.py | 0 {test => tests}/test_common/__init__.py | 0 {test => tests}/test_common/test_coalesce.py | 0 {test => tests}/test_common/test_file_operations.py | 0 {test => tests}/test_common/test_serialization.py | 0 {test => tests}/test_kkp/__init__.py | 0 {test => tests}/test_kkp/test_projects.py | 0 {test => tests}/test_kksubs/__init__.py | 0 {test => tests}/test_kksubs/test_font_handling.py | 0 {test => tests}/test_kksubs/test_serialization.py | 0 {test => tests}/test_kksubs/test_subtitling.py | 0 uv.lock | 12 ++++++------ 13 files changed, 10 insertions(+), 10 deletions(-) rename {test => tests}/__init__.py (100%) rename {test => tests}/test_common/__init__.py (100%) rename {test => tests}/test_common/test_coalesce.py (100%) rename {test => tests}/test_common/test_file_operations.py (100%) rename {test => tests}/test_common/test_serialization.py (100%) rename {test => tests}/test_kkp/__init__.py (100%) rename {test => tests}/test_kkp/test_projects.py (100%) rename {test => tests}/test_kksubs/__init__.py (100%) rename {test => tests}/test_kksubs/test_font_handling.py (100%) rename {test => tests}/test_kksubs/test_serialization.py (100%) rename {test => tests}/test_kksubs/test_subtitling.py (100%) diff --git a/pyproject.toml b/pyproject.toml index a06b43d..8f12168 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,13 +13,13 @@ dependencies = [ "packaging", "pillow", "pyyaml", - "ruff>=0.14.14", ] [project.optional-dependencies] -test = [ +dev = [ "pytest", "pytest-cov", + "ruff>=0.14.14", ] [project.scripts] @@ -27,7 +27,7 @@ koi = "koi.cli:cli" [tool.setuptools.packages.find] where = ["src"] -exclude = ["test*"] +exclude = ["tests*"] [tool.setuptools] include-package-data = true @@ -36,7 +36,7 @@ include-package-data = true "resources.fonts.roboto" = ["*.ttf"] [tool.pytest.ini_options] -testpaths = ["test"] +testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/test/test_common/__init__.py b/tests/test_common/__init__.py similarity index 100% rename from test/test_common/__init__.py rename to tests/test_common/__init__.py diff --git a/test/test_common/test_coalesce.py b/tests/test_common/test_coalesce.py similarity index 100% rename from test/test_common/test_coalesce.py rename to tests/test_common/test_coalesce.py diff --git a/test/test_common/test_file_operations.py b/tests/test_common/test_file_operations.py similarity index 100% rename from test/test_common/test_file_operations.py rename to tests/test_common/test_file_operations.py diff --git a/test/test_common/test_serialization.py b/tests/test_common/test_serialization.py similarity index 100% rename from test/test_common/test_serialization.py rename to tests/test_common/test_serialization.py diff --git a/test/test_kkp/__init__.py b/tests/test_kkp/__init__.py similarity index 100% rename from test/test_kkp/__init__.py rename to tests/test_kkp/__init__.py diff --git a/test/test_kkp/test_projects.py b/tests/test_kkp/test_projects.py similarity index 100% rename from test/test_kkp/test_projects.py rename to tests/test_kkp/test_projects.py diff --git a/test/test_kksubs/__init__.py b/tests/test_kksubs/__init__.py similarity index 100% rename from test/test_kksubs/__init__.py rename to tests/test_kksubs/__init__.py diff --git a/test/test_kksubs/test_font_handling.py b/tests/test_kksubs/test_font_handling.py similarity index 100% rename from test/test_kksubs/test_font_handling.py rename to tests/test_kksubs/test_font_handling.py diff --git a/test/test_kksubs/test_serialization.py b/tests/test_kksubs/test_serialization.py similarity index 100% rename from test/test_kksubs/test_serialization.py rename to tests/test_kksubs/test_serialization.py diff --git a/test/test_kksubs/test_subtitling.py b/tests/test_kksubs/test_subtitling.py similarity index 100% rename from test/test_kksubs/test_subtitling.py rename to tests/test_kksubs/test_subtitling.py diff --git a/uv.lock b/uv.lock index 750c355..fbc782b 100644 --- a/uv.lock +++ b/uv.lock @@ -152,13 +152,13 @@ dependencies = [ { name = "packaging" }, { name = "pillow" }, { name = "pyyaml" }, - { name = "ruff" }, ] [package.optional-dependencies] -test = [ +dev = [ { name = "pytest" }, { name = "pytest-cov" }, + { name = "ruff" }, ] [package.metadata] @@ -168,12 +168,12 @@ requires-dist = [ { name = "opencv-python" }, { name = "packaging" }, { name = "pillow" }, - { name = "pytest", marker = "extra == 'test'" }, - { name = "pytest-cov", marker = "extra == 'test'" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-cov", marker = "extra == 'dev'" }, { name = "pyyaml" }, - { name = "ruff", specifier = ">=0.14.14" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.14.14" }, ] -provides-extras = ["test"] +provides-extras = ["dev"] [[package]] name = "natsort" From bc5d5e18c184d36cde000970740dc3d1ad6ab648 Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:14:28 -0800 Subject: [PATCH 05/13] add newline before end of files --- src/common/data/file.py | 2 +- src/common/data/representable.py | 2 +- src/common/exceptions.py | 2 +- src/common/import_utils.py | 2 +- src/common/utils/coalesce.py | 2 +- src/common/watcher/file_change.py | 2 +- src/kkp/cmd/project.py | 2 +- src/kkp/controller/project.py | 2 +- src/kkp/data/config.py | 2 +- src/kkp/service/studio_project.py | 2 +- src/kkp/view/project.py | 2 +- src/kkp/watcher/project.py | 2 +- src/kksubs/cmd/subtitle.py | 2 +- src/kksubs/controller/subtitle.py | 2 +- src/kksubs/data/abstract.py | 2 +- src/kksubs/data/subtitle/style.py | 2 +- src/kksubs/data/subtitle/style_attributes.py | 2 +- src/kksubs/data/subtitle/style_row_enum.py | 2 +- src/kksubs/service/extraction/style.py | 2 +- src/kksubs/service/extraction/subtitle.py | 2 +- src/kksubs/service/file.py | 2 +- src/kksubs/service/processor/motion_blur.py | 2 +- src/kksubs/service/processor/utils/image.py | 2 +- src/kksubs/service/sub_project.py | 2 +- src/kksubs/utils/renamer.py | 2 +- src/kksubs/utils/sanitizers.py | 2 +- src/kksubs/watcher/subtitle.py | 2 +- src/koi/app/controller.py | 2 +- src/koi/cli.py | 2 +- tests/test_common/test_serialization.py | 2 +- tests/test_kksubs/test_font_handling.py | 2 +- tests/test_kksubs/test_serialization.py | 2 +- tests/test_kksubs/test_subtitling.py | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/common/data/file.py b/src/common/data/file.py index 17a9a7e..f43f945 100644 --- a/src/common/data/file.py +++ b/src/common/data/file.py @@ -124,4 +124,4 @@ def state(self): 'time': self.read_time, 'files': self.files, 'folders': self.folders, - } \ No newline at end of file + } diff --git a/src/common/data/representable.py b/src/common/data/representable.py index 1330d62..d568767 100644 --- a/src/common/data/representable.py +++ b/src/common/data/representable.py @@ -23,4 +23,4 @@ def serialize(self) -> Dict: attributes[attribute] = value.serialize() else: attributes[attribute] = value - return attributes \ No newline at end of file + return attributes diff --git a/src/common/exceptions.py b/src/common/exceptions.py index b4b4650..16a2944 100644 --- a/src/common/exceptions.py +++ b/src/common/exceptions.py @@ -14,4 +14,4 @@ def __init__(self, *args: object) -> None: class NotConfiguredException(Exception): def __init__(self): - super().__init__("Application is not yet configured, run `koi configure` first.") \ No newline at end of file + super().__init__("Application is not yet configured, run `koi configure` first.") diff --git a/src/common/import_utils.py b/src/common/import_utils.py index c4884f7..46f8c60 100644 --- a/src/common/import_utils.py +++ b/src/common/import_utils.py @@ -3,4 +3,4 @@ KKSUBS_VERSION = importlib.metadata.version('kksubs') def get_kksubs_version(): - return KKSUBS_VERSION \ No newline at end of file + return KKSUBS_VERSION diff --git a/src/common/utils/coalesce.py b/src/common/utils/coalesce.py index 0253e53..3e969d2 100644 --- a/src/common/utils/coalesce.py +++ b/src/common/utils/coalesce.py @@ -2,4 +2,4 @@ def coalesce(*args): for arg in args: if arg is not None: return arg - return None \ No newline at end of file + return None diff --git a/src/common/watcher/file_change.py b/src/common/watcher/file_change.py index 616181b..37885e8 100644 --- a/src/common/watcher/file_change.py +++ b/src/common/watcher/file_change.py @@ -30,4 +30,4 @@ def is_event_trigger(self) -> bool: change_detected = change_detected or changed_file self.mtsum_by_file[file] = new_mtsum - return change_detected \ No newline at end of file + return change_detected diff --git a/src/kkp/cmd/project.py b/src/kkp/cmd/project.py index 20c70de..ad6c202 100644 --- a/src/kkp/cmd/project.py +++ b/src/kkp/cmd/project.py @@ -150,4 +150,4 @@ def command_line(): if command is None: controller.info(args.recent) # display information, version, current project - controller.close() \ No newline at end of file + controller.close() diff --git a/src/kkp/controller/project.py b/src/kkp/controller/project.py index 09b20aa..259dc38 100644 --- a/src/kkp/controller/project.py +++ b/src/kkp/controller/project.py @@ -1032,4 +1032,4 @@ def close(self): # persist changes to metadata self._write_config() self._write_data() - logger.info('Closing project controller...') \ No newline at end of file + logger.info('Closing project controller...') diff --git a/src/kkp/data/config.py b/src/kkp/data/config.py index 66155f0..57158b6 100644 --- a/src/kkp/data/config.py +++ b/src/kkp/data/config.py @@ -98,4 +98,4 @@ def deserialize(self, settings_data:Dict): export_settings=ExportSettings.deserialize(settings_data.get(ExportSettings.name)), log_settings=LogSettings.deserialize(settings_data.get(LogSettings.name)), open_game_directory_settings=OpenGameDirectorySettings.deserialize(settings_data.get(OpenGameDirectorySettings.name)), - ) \ No newline at end of file + ) diff --git a/src/kkp/service/studio_project.py b/src/kkp/service/studio_project.py index 5e575af..ba81a0a 100644 --- a/src/kkp/service/studio_project.py +++ b/src/kkp/service/studio_project.py @@ -386,4 +386,4 @@ def correct_scene_order(self) -> bool: return True except Exception as e: logger.error(f"Error correcting scene order: {e}") - return False \ No newline at end of file + return False diff --git a/src/kkp/view/project.py b/src/kkp/view/project.py index 6cd21f2..aa651dd 100644 --- a/src/kkp/view/project.py +++ b/src/kkp/view/project.py @@ -45,4 +45,4 @@ def confirm_project_checkout(self, source, destination) -> bool: print(f' - source: {source}') print(f' - destination: {destination}') confirm = input('Enter Y to confirm: ') == 'Y' - return confirm \ No newline at end of file + return confirm diff --git a/src/kkp/watcher/project.py b/src/kkp/watcher/project.py index 0a64365..4f9ee0c 100644 --- a/src/kkp/watcher/project.py +++ b/src/kkp/watcher/project.py @@ -20,4 +20,4 @@ def pass_sync(self, sync_func:callable): def event_trigger_action(self): self.sync_func() - return super().event_trigger_action() \ No newline at end of file + return super().event_trigger_action() diff --git a/src/kksubs/cmd/subtitle.py b/src/kksubs/cmd/subtitle.py index d7b6c09..c11935b 100644 --- a/src/kksubs/cmd/subtitle.py +++ b/src/kksubs/cmd/subtitle.py @@ -108,4 +108,4 @@ def command_line(): controller.info() controller.close() - return \ No newline at end of file + return diff --git a/src/kksubs/controller/subtitle.py b/src/kksubs/controller/subtitle.py index 38ac18c..5ed6457 100644 --- a/src/kksubs/controller/subtitle.py +++ b/src/kksubs/controller/subtitle.py @@ -93,4 +93,4 @@ def clear(self): def close(self): logger.info('Closing Subtitle controller.') - return \ No newline at end of file + return diff --git a/src/kksubs/data/abstract.py b/src/kksubs/data/abstract.py index f1d9420..b08dc2d 100644 --- a/src/kksubs/data/abstract.py +++ b/src/kksubs/data/abstract.py @@ -25,4 +25,4 @@ def corrected(self): self.correct_values() return self - pass \ No newline at end of file + pass diff --git a/src/kksubs/data/subtitle/style.py b/src/kksubs/data/subtitle/style.py index 0094653..8a17e64 100644 --- a/src/kksubs/data/subtitle/style.py +++ b/src/kksubs/data/subtitle/style.py @@ -256,4 +256,4 @@ def deserialize(self, matrix_data) -> "StyleMatrix": if row_datas is None or not row_datas: return StyleMatrix() rows = list(map(StyleRow.deserialize, row_datas)) - return StyleMatrix(rows=rows) \ No newline at end of file + return StyleMatrix(rows=rows) diff --git a/src/kksubs/data/subtitle/style_attributes.py b/src/kksubs/data/subtitle/style_attributes.py index 79127a2..f25492b 100644 --- a/src/kksubs/data/subtitle/style_attributes.py +++ b/src/kksubs/data/subtitle/style_attributes.py @@ -369,4 +369,4 @@ def coalesce(self, other:"Background"): def correct_values(self): return - pass \ No newline at end of file + pass diff --git a/src/kksubs/data/subtitle/style_row_enum.py b/src/kksubs/data/subtitle/style_row_enum.py index 128eab1..bd0cfa3 100644 --- a/src/kksubs/data/subtitle/style_row_enum.py +++ b/src/kksubs/data/subtitle/style_row_enum.py @@ -26,4 +26,4 @@ def get(cls, row_name) -> StyleRow: for r_enum in cls: if row_name == r_enum.name: return r_enum.value - return None \ No newline at end of file + return None diff --git a/src/kksubs/service/extraction/style.py b/src/kksubs/service/extraction/style.py index b873c96..336ffd9 100644 --- a/src/kksubs/service/extraction/style.py +++ b/src/kksubs/service/extraction/style.py @@ -94,4 +94,4 @@ def extract_styles(styles_contents:List[dict]) -> Dict[str, Style]: _process_entity(style_data, styles) logger.debug(f'Obtained styles {styles.keys()}.') - return styles \ No newline at end of file + return styles diff --git a/src/kksubs/service/extraction/subtitle.py b/src/kksubs/service/extraction/subtitle.py index 8bdbf02..81732e7 100644 --- a/src/kksubs/service/extraction/subtitle.py +++ b/src/kksubs/service/extraction/subtitle.py @@ -217,4 +217,4 @@ def extract_subtitle_groups( subtitle_groups_by_image_id[image_id] = subtitle_groups - return subtitle_groups_by_image_id \ No newline at end of file + return subtitle_groups_by_image_id diff --git a/src/kksubs/service/file.py b/src/kksubs/service/file.py index e623ac4..9e43d9b 100644 --- a/src/kksubs/service/file.py +++ b/src/kksubs/service/file.py @@ -45,4 +45,4 @@ def sync_bidirectional( if os.path.isdir(source): output = sync_bidirectional(source, destination, previous_state) - return output \ No newline at end of file + return output diff --git a/src/kksubs/service/processor/motion_blur.py b/src/kksubs/service/processor/motion_blur.py index 6bfa51f..9846a03 100644 --- a/src/kksubs/service/processor/motion_blur.py +++ b/src/kksubs/service/processor/motion_blur.py @@ -68,4 +68,4 @@ def apply_motion_blur(image:Image.Image, kernel_size=None, angle=None) -> Image. rotated_mb = cv2.filter2D(cv2_image, -1, rotated_kernel) blurred_image = Image.fromarray(cv2.cvtColor(rotated_mb, cv2.COLOR_BGR2RGB)) # convert cv2 to image. - return blurred_image \ No newline at end of file + return blurred_image diff --git a/src/kksubs/service/processor/utils/image.py b/src/kksubs/service/processor/utils/image.py index 6839bb6..9032fd4 100644 --- a/src/kksubs/service/processor/utils/image.py +++ b/src/kksubs/service/processor/utils/image.py @@ -13,4 +13,4 @@ def in_cv2_environment(fn): # creates a cv2 environment for a cv2-valued function. def decorated_fn(image, *args, **kwargs): return cv2_to_pil(fn(pil_to_cv2(image), *args, **kwargs)) - return decorated_fn \ No newline at end of file + return decorated_fn diff --git a/src/kksubs/service/sub_project.py b/src/kksubs/service/sub_project.py index 14b19d6..c8223f3 100644 --- a/src/kksubs/service/sub_project.py +++ b/src/kksubs/service/sub_project.py @@ -566,4 +566,4 @@ def delete_project(self): if os.path.isdir(path): shutil.rmtree(path) if os.path.isfile(path): - os.remove(path) \ No newline at end of file + os.remove(path) diff --git a/src/kksubs/utils/renamer.py b/src/kksubs/utils/renamer.py index 6fbaee5..6aec8bf 100644 --- a/src/kksubs/utils/renamer.py +++ b/src/kksubs/utils/renamer.py @@ -107,4 +107,4 @@ def update_images_in_textpath(text_path:str, image_paths:List[str], new_image_pa if content == updated_textstring: # no need to update. return with open(text_path, "w", encoding="utf-8") as writer: - writer.write(updated_textstring) \ No newline at end of file + writer.write(updated_textstring) diff --git a/src/kksubs/utils/sanitizers.py b/src/kksubs/utils/sanitizers.py index 8bedc25..d6f0392 100644 --- a/src/kksubs/utils/sanitizers.py +++ b/src/kksubs/utils/sanitizers.py @@ -45,4 +45,4 @@ def to_xy_coords(value) -> Optional[tuple]: def to_string(value) -> Optional[str]: if value is None or isinstance(value, str): return value - raise TypeError(type(value)) \ No newline at end of file + raise TypeError(type(value)) diff --git a/src/kksubs/watcher/subtitle.py b/src/kksubs/watcher/subtitle.py index 8c3193f..a14fdd8 100644 --- a/src/kksubs/watcher/subtitle.py +++ b/src/kksubs/watcher/subtitle.py @@ -49,4 +49,4 @@ def event_trigger_action(self): def event_idle_action(self): formatted_time = self.time() print(f"{formatted_time} No changes detected.") - logger.info(f"{formatted_time} No changes detected.") \ No newline at end of file + logger.info(f"{formatted_time} No changes detected.") diff --git a/src/koi/app/controller.py b/src/koi/app/controller.py index b9dd4a3..4fddb0b 100644 --- a/src/koi/app/controller.py +++ b/src/koi/app/controller.py @@ -57,4 +57,4 @@ def configure_application(): yaml.safe_dump(config, writer) logger.info("Application configure success.") - return 0 \ No newline at end of file + return 0 diff --git a/src/koi/cli.py b/src/koi/cli.py index ed34979..51f4730 100644 --- a/src/koi/cli.py +++ b/src/koi/cli.py @@ -130,4 +130,4 @@ def cli(): controller.export_gallery() pass - controller.close() \ No newline at end of file + controller.close() diff --git a/tests/test_common/test_serialization.py b/tests/test_common/test_serialization.py index 08cea3f..a4c94e2 100644 --- a/tests/test_common/test_serialization.py +++ b/tests/test_common/test_serialization.py @@ -11,4 +11,4 @@ # Placeholder test to prevent pytest from complaining about empty test file def test_placeholder(): """Placeholder test - actual tests need to be implemented.""" - pass \ No newline at end of file + pass diff --git a/tests/test_kksubs/test_font_handling.py b/tests/test_kksubs/test_font_handling.py index cf11a8d..235ea86 100644 --- a/tests/test_kksubs/test_font_handling.py +++ b/tests/test_kksubs/test_font_handling.py @@ -109,4 +109,4 @@ def test_default_font(): # Should return an image of the same size assert result_image.size == test_image.size except Exception as e: - pytest.fail(f"Default font caused crash: {e}") \ No newline at end of file + pytest.fail(f"Default font caused crash: {e}") diff --git a/tests/test_kksubs/test_serialization.py b/tests/test_kksubs/test_serialization.py index b0294ae..efb801e 100644 --- a/tests/test_kksubs/test_serialization.py +++ b/tests/test_kksubs/test_serialization.py @@ -33,4 +33,4 @@ def test_subtitle_serialization(): def test_subtitle_group_serialization(): - assert_representations_equal(SubtitleGroup(), SubtitleGroup) \ No newline at end of file + assert_representations_equal(SubtitleGroup(), SubtitleGroup) diff --git a/tests/test_kksubs/test_subtitling.py b/tests/test_kksubs/test_subtitling.py index f8c58f8..2d6e7ac 100644 --- a/tests/test_kksubs/test_subtitling.py +++ b/tests/test_kksubs/test_subtitling.py @@ -58,4 +58,4 @@ def test_add_subtitles(controller_setup, test_images): assert len(os.listdir(controller.get_scripts_directory())) == 1 controller.add_subtitles(allow_multiprocessing=False) script = controller.get_scripts()[0] - assert sorted(os.listdir(controller.get_output_directory_by_script(script))) == sorted(test_images) \ No newline at end of file + assert sorted(os.listdir(controller.get_output_directory_by_script(script))) == sorted(test_images) From d1b300502201e266a0885defa9dcd51d02981379 Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:17:31 -0800 Subject: [PATCH 06/13] update license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index fc6c924..bf445d8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 ptf +Copyright (c) 2023 psilabs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 553a65a40f26b5147d9d547a0dd80e44b513d864 Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:22:46 -0800 Subject: [PATCH 07/13] update docs --- README.md | 4 +- docs/README.md | 10 +- docs/architecture.md | 366 ++++++++++++++++++++++++++++++++++++ docs/command_line/kkp.md | 160 ---------------- docs/command_line/kksubs.md | 30 --- docs/command_line/koi.md | 323 ++++++++++++++++++++++++++++--- src/koi/cli.py | 14 ++ 7 files changed, 681 insertions(+), 226 deletions(-) create mode 100644 docs/architecture.md delete mode 100644 docs/command_line/kkp.md delete mode 100644 docs/command_line/kksubs.md diff --git a/README.md b/README.md index 2c5597a..73d5500 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ![processed-image](demo.png) ## Installation and Setup -The program is intended to work on a Windows OS with Python versions 3.10-3.12. +The program is intended to work on a Windows OS with Python 3.10 or later. Open a Python virtual environment and install the Git repository. For example: ```console @@ -19,4 +19,4 @@ Configure the application: ```console $ koi configure ``` -Command line reference ([koi](docs\command_line\koi.md)). \ No newline at end of file +Command line reference ([koi](docs/command_line/koi.md)). \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 7748121..9489690 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,7 +15,11 @@ Drafts are the main way of adding content to an image. The following items are a 1. [Style fields](subtitle_project/styles.md) ## Command line tools -Commands are the primary way of adding subtitles for a subtitle project. +The `koi` command line tool is the primary interface for project management and subtitle processing. -1. [kksubs command line](command_line/kksubs.md): simple subtitling tool (no game required). -1. [kkp command line](command_line/kkp.md): subtitle project management and integration with the Koikatsu Party/Chara Studio game; extends `kksubs`. +1. [koi command line](command_line/koi.md): unified CLI for project management and subtitle automation. + +## Architecture +Understanding the system design and project structure. + +1. [Architecture](architecture.md): system architecture, synchronization flow, and design patterns. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..59fa8a3 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,366 @@ +# kksubs Architecture + +This document describes the architecture and design principles of the kksubs subtitle automation system. + +## Overview + +kksubs is a Python-based subtitle automation tool for Koikatsu Party CharaStudio scenes. It manages projects (collections of game data and subtitle files) that can be versioned, branched, and merged like save files or game cartridges. + +### Core Concepts + +#### The Cartridge Analogy + +Think of kksubs like a game console with swappable cartridges: + +- **Library**: Your collection of game cartridges (all saved projects) +- **Project**: A single cartridge containing both game data and subtitle definitions +- **Game Directory**: The console itself (only holds one cartridge at a time) +- **Workspace**: The subtitle processor that works with captures from the loaded cartridge +- **Active Project**: The cartridge currently inserted in the console + +When you "checkout" a project, you're swapping cartridges - the new project's data loads into both the game directory and workspace. + +#### Components + +**Library** (`~/library/`) +- Persistent storage for all projects +- Organized hierarchically: `library/genre/project-name/` +- Each project contains: + - `UserData/` - Game assets (characters, scenes, backgrounds, captures) + - `kksubs-project/` - Subtitle data (drafts, styles, outputs) + +**Game Directory** (`C:\...\Koikatsu Party\`) +- Koikatsu Party installation +- Contains `UserData/` folder with currently loaded project data +- Only one project can be "loaded" at a time +- Captures are saved to `UserData/cap/` + +**Workspace** (`~/workspace/`) +- Subtitle processing environment +- Structure: + ``` + workspace/ + ├── images/ # Synced from game UserData/cap + ├── drafts/ # Text files defining subtitles + │ ├── draft-1.txt + │ └── draft-2.txt + ├── output/ # Generated subtitled images + │ ├── draft-1/ + │ └── draft-2/ + └── styles.yml # Style definitions + ``` + +**Active Project** +- The project currently loaded in the game directory and workspace +- Stored in metadata: `~/.kksubs/metadata/data.yaml` +- Represents the "sharded" state across game and workspace + +--- + +## Synchronization Architecture + +The active project's state is distributed across three locations. Synchronization keeps them consistent. + +### Synchronization Flow Diagram + +``` +┌─────────────────────────────────────────────────────┐ +│ Library │ +│ (Persistent Backup) │ +│ │ +│ genre/story-1/ │ +│ ├── UserData/ ┌──────────────────┐ │ +│ │ ├── bg/ │ Bidirectional │ │ +│ │ ├── chara/ │ Sync │ │ +│ │ ├── studio/ │ │ │ +│ │ └── cap/ │ Tracks changes │ │ +│ │ │ via journal │ │ +│ └── kksubs-project/ │ (sync-state) │ │ +│ ├── drafts/ └──────────────────┘ │ +│ ├── output/ │ +│ └── styles.yml │ +└─────────────────────────────────────────────────────┘ + ▲ ▲ + │ │ + │ Bidirectional │ Bidirectional + │ Sync │ Sync + │ │ + ▼ ▼ +┌─────────────────────┐ ┌─────────────────────────┐ +│ Game Directory │ │ Workspace │ +│ (Currently Loaded) │ │ (Subtitle Processor) │ +│ │ │ │ +│ UserData/ │ │ ~/workspace/ │ +│ ├── bg/ │ │ ├── images/ │ +│ ├── chara/ │ │ ├── drafts/ │ +│ ├── studio/ │────────→│ ├── output/ │ +│ └── cap/ │ │ │ └── styles.yml │ +│ └── *.png │ │ │ │ +└─────────────────────┘ │ └─────────────────────────┘ + │ + Unidirectional + (cap → images) + Screenshots flow + one-way only +``` + +### Synchronization Types + +**Bidirectional Sync** (`game/UserData` ↔ `library/UserData`) +- Changes in either location are mirrored to the other +- Uses journal-based tracking (`studio-sync-state` in metadata) +- Handles additions, modifications, and deletions +- Executed during `koi sync` and `koi checkout` + +**Unidirectional Sync** (`game/UserData/cap` → `workspace/images`) +- Screenshots from game flow into workspace +- One-way only: workspace doesn't write back to game +- New captures automatically appear in workspace +- Executed during `koi sync` and `koi run` + +**Bidirectional Sync** (`workspace/` ↔ `library/kksubs-project/`) +- Subtitle files (drafts, outputs, styles) sync both ways +- Uses journal-based tracking (`subtitle-sync-state` in metadata) +- Preserves work in both locations +- Executed during `koi sync` and `koi checkout` + +### Synchronization Implementation + +**Journal-Based Change Detection** +- Each sync operation records file states in metadata +- On next sync, compares current state to journal +- Uses set theory to identify: new files, modified files, deleted files +- Multiprocessing for batch operations + +**Incremental Subtitle Updates** +- Only regenerates subtitles when inputs change: + - Draft file modified + - Style file modified + - Source image changed +- Tracks state in workspace metadata (`.kksubs/` directory) +- Significantly faster than full regeneration + +--- + +## Project Lifecycle + +### Creating a Project + +```sh +koi archive create my-story +``` + +1. Copies `game/UserData/` → `library/my-story/UserData/` +2. Copies `workspace/` → `library/my-story/kksubs-project/` +3. Updates metadata with project info +4. Records sync states + +### Checking Out a Project + +```sh +koi checkout my-story +``` + +1. Syncs current project back to library (if any) +2. Clears game `UserData/` directory +3. Copies `library/my-story/UserData/` → `game/UserData/` +4. Copies `library/my-story/kksubs-project/` → `workspace/` +5. Updates `current-project` in metadata +6. Records new sync states + +### Branching a Project + +```sh +koi checkout -b my-story-variant +``` + +1. Creates new project in library from current project +2. Names it `my-story-variant` (in same directory as parent) +3. Loads the new project into game and workspace +4. Both projects now exist independently in library + +### Merging Projects + +```sh +koi merge other-story +``` + +1. Prompts for confirmation +2. Copies specific UserData folders from `library/other-story/` → `game/` +3. Merged folders: `bg`, `chara`, `coordinate`, `MaterialEditor`, `Overlays`, `studio` +4. Excluded: `cap` (screenshots), `config`, `save` +5. Files with same names are overridden +6. Does NOT affect library - only current game directory + +--- + +## Subtitle Processing Workflow + +### One-Time Execution + +```sh +koi run +``` + +1. **Sync**: Update workspace images from game captures +2. **Parse**: Read all draft files in `workspace/drafts/` +3. **Extract**: Parse `workspace/styles.yml` for style definitions +4. **Process**: For each draft: + - Match subtitles to images + - Apply styles (text, outlines, effects) + - Render subtitled images + - Save to `workspace/output//` +5. **Sync**: Update library with new outputs + +### Watch Mode + +```sh +koi run --forever +``` + +Starts background watchers monitoring: +- `game/UserData/cap/` - New screenshots +- `workspace/drafts/` - Draft changes +- `workspace/styles.yml` - Style changes +- `workspace/images/` - Image additions/deletions + +On change detected: +1. Run sync +2. Regenerate affected subtitles only (incremental) +3. Update library + +Press Ctrl+C to stop. + +--- + +## Image Processing Pipeline + +### Core Libraries + +- **Pillow**: Text rendering, composition, effects +- **OpenCV**: Advanced image operations, color space conversions +- **NumPy**: Array operations, transformations + +### Processing Steps + +For each subtitle: + +1. **Load Image**: Read source PNG from workspace/images +2. **Parse Subtitle**: Extract text, position, style from draft +3. **Apply Style**: Retrieve style definition (with inheritance) +4. **Render Text**: + - Load font (bundled Roboto or custom) + - Create text layer with alpha channel + - Apply stroke (outline) +5. **Apply Effects**: + - Multiple outlines (feathered edges via blur) + - Gaussian blur + - Motion blur (directional) + - Brightness adjustment +6. **Apply Box Transformations**: + - Position via anchor coordinates or grid system + - Rotation + - Nudge (fine-tuning) +7. **Composite**: + - Blend text onto image + - Apply asset overlays (if any) + - Apply masks (if any) +8. **Save**: Write to output directory + +### Style System + +Styles support: +- **Inheritance**: Styles can extend other styles +- **Matrix Multiplication**: Define style rows that combine (e.g., grid positions × text colors) +- **Per-Subtitle Overrides**: Individual subtitles can override any style property + +See [Style Documentation](subtitle_project/styles.md) for details. + +--- + +## Data Structures + +### Configuration (`~/.kksubs/config.yaml`) + +```yaml +game-directory: C:\Games\Koikatsu Party +library-directory: C:\Users\...\library +workspace-directory: C:\Users\...\workspace +settings: + export: + destination: C:\Users\...\exports + log: + level: warning +``` + +### Metadata (`~/.kksubs/metadata/data.yaml`) + +```yaml +current-project: genre/my-story +subtitle-sync-state: + # Journal of workspace ↔ library sync + workspace/drafts/draft-1.txt: {mtime: 1234567890, hash: "..."} + workspace/output/draft-1/image-1.png: {mtime: 1234567891, hash: "..."} +studio-sync-state: + # Journal of game ↔ library sync + UserData/chara/char-1.png: {mtime: 1234567892, hash: "..."} + UserData/studio/scene-1.png: {mtime: 1234567893, hash: "..."} +recent-projects: + - genre/my-story + - action/beach-episode + - ... +version: 3.2.0-dev +``` + +### Draft Format + +Text-based format defining subtitles: + +``` +image_id: screenshot-001.png +content: Hello, world! +text_data.color: [255, 255, 255, 255] +box_data.anchor: [0, -200] + +image_id: screenshot-002.png +content: | + Multi-line + subtitle text +style: custom-style-id +``` + +See [Draft Documentation](subtitle_project/draft.md) for syntax. + +--- + +## Development Architecture + +### Module Organization + +``` +src/ +├── common/ # Shared utilities +│ ├── data/ # Serialization (RepresentableData) +│ ├── utils/ # File ops, coalesce, decorators +│ └── watcher/ # File watching abstraction +│ +├── kkp/ # Project management +│ ├── controller/ # ProjectController (business logic) +│ ├── service/ # StudioProjectService (library ops) +│ ├── view/ # ProjectView (display) +│ └── watcher/ # ProjectWatcher (file watching) +│ +├── kksubs/ # Subtitle processing +│ ├── controller/ # SubtitleController +│ ├── service/ +│ │ ├── extraction/ # Parse drafts/styles +│ │ ├── processor/ # Image rendering +│ │ └── sub_project.py # SubtitleProjectService +│ └── watcher/ # SubtitleWatcher +│ +├── koi/ # CLI entry point +│ ├── cli.py # Argument parsing +│ └── app/ # Application config +│ +└── resources/ # Static assets (fonts) +``` diff --git a/docs/command_line/kkp.md b/docs/command_line/kkp.md deleted file mode 100644 index 1f76a60..0000000 --- a/docs/command_line/kkp.md +++ /dev/null @@ -1,160 +0,0 @@ -# KKP Command Line Tool (deprecated) - -> `kkp` is deprecated, use `koi` instead. - -`kkp` (Koikatsu Projects) is a command line tool that integrates subtitle projects with the Koikatsu Party game in a more integrated environment. - -A "studio project" refers to the subtitle project along with the `UserData` folder it is associated with. `kkp` is thus a manager of studio projects. - -Main features: - -* Create a subtitle project from the current game directory - * Add subtitles on images in the `UserData/cap` folder -* Save/load multiple studio projects to/from a library - -## Configure -```bash -kkp --workspace [workspace] --game [game-directory] --library [library-directory] -``` - -Configures the `kkp` command line tool. Configuration is stored in `[working-directory]/config.yaml`, while data is stored in `[working-directory]/data.yaml`. - -The `[game-directory]` is the game directory which contains the `UserData` folder. The `[workspace]` is the folder where subtitle-related objects go (e.g. `images`, `output`, `drafts`, `styles.yml`). - -### Example -``` -kkp --workspace workspace --game "C:\Users\user\Games\Koikatsu Party" --library library -``` - ---- -## Create Project -```bash -kkp create [project-name] -``` -Makes a copy of `UserData` in the game directory into the library, and also copies the renders in `UserData/cap` to `[workspace]/images`. This is known as the "current project". Additionally, creates a subtitle project: -``` -- [workspace] - - images - - # the captures from the game directory's UserData/cap - - drafts - - output - - styles.yml -``` - -## List Projects -```bash -kkp list -p [pattern] -# [0] project-1 -# [1] project-2 -# ... -``` -List valid projects found in the `[library]`. The list can be filtered with a pattern using wildcards. - -## Synchronize -Saves and synchronizes files between the `[game]`, `[library]`, and `[workspace]`. In arrows (one-way and two-way sync resp.): - -``` -[game]/UserData <==> [library]/[current-project]/UserData -[library]/[current-project]/UserData/cap ==> [workspace]/images - -[workspace]/drafts <==> [library]/[current-project]/kksubs-project/drafts -[workspace]/output <==> [library]/[current-project]/kksubs-project/output -[workspace]/styles.yml <==> [library]/[current-project]/kksubs-project/styles.yml -``` - -## Checkout Project -```bash -kkp checkout [project-name] -``` -Change the current project to another project in the library. This replaces the game directory's `UserData` with the one found under `[library]/[project-name]`. This also replaces the items in the subtitle workspace with the ones found under `[library]/[project-name]/kksubs-project`. - -Checkout to a new project from the current one like git. (This will create a project in the same directory as the current project.) -```bash -kkp checkout --branch [project-name] -``` -Combine `list` and `checkout` to checkout based on numbers. -```bash -kkp list -p *world* -# [0] hello/world -kkp checkout 0 -# checkout project hello/world -``` - -## Execute Programs/Open Folders - -``` -kkp game -``` -Run Koikatsu Party. -``` -kkp studio -``` -Run Chara Studio. -``` -kkp game-folder --shortcut [shortcut] -``` -Open Koikatsu Party folder with explorer. Optional shortcuts (e.g. `cap` or `chara`) open the `cap` or `chara` folders respectively, and can be modified in `config.yaml` settings. -``` -kkp show -``` -Open subtitled image folders with explorer. - -## Delete Project -```bash -kkp delete [project-name] -``` -Delete project with name `[project-name]` from the library. - -If the current project is to be deleted, the contents of the workspace will be deleted too. - -## Compose/Activate -```bash -kkp compose -``` -Create subtitled images once. The images are saved in the `output` directory. -```bash -kkp activate -``` -Continuously watch the `[workspace]` and `[game]/UserData/cap` for changes, and applies two actions as observed files change: - -1. runs `kkp sync` -2. runs `kkp compoose` - -Furthermore, uses incremental updating to process only images whose image, subtitle or style has changed, accelerating the subtitling process. - -Persistent data used to accelerate subtitling is stored in the `[workspace]/.kksubs` directory. This is not necessary to add subtitles, but enables incremental updating. - -## Clear -```bash -kkp clear -``` -Delete outputs and the `.kksubs` directory within the workspace. - -## Merge -``` -kkp merge [project-name] -``` -Merge `UserData` contents of another project (or projects if you use wildcards) into the current one. Existing files will not be deleted, but files with the same names will be overridden. - -Affected folders include: - -* bg -* cap -* cardframe -* chara -* coordinate -* MaterialEditor -* Overlays -* studio - -## Export -``` -kkp export [--clean] [--show] [--force] [export-destination] -``` -Export outputs (`UserData/cap` and `kksubs-project/output`) of projects to a destination folder. - -`clean` flag deletes the destination folder and creates a new one. - -`show` flag opens the destination folder in file explorer after exporting. - -`force` flag disables confirmation prompts. \ No newline at end of file diff --git a/docs/command_line/kksubs.md b/docs/command_line/kksubs.md deleted file mode 100644 index ef9f579..0000000 --- a/docs/command_line/kksubs.md +++ /dev/null @@ -1,30 +0,0 @@ -# KKSUBS Command Line Tool - -`kksubs` (Koikatsu Subtitles) is a command line tool that adds subtitles to images. - -## Initialize Subtitle Project -```bash -kksubs --project [project-directory] init -``` -Create a blank subtitling project with structure -``` -- images -- drafts -- output -- styles.yml -``` -Does not override existing project structure. Defaults to current directory. - -## Rename -```bash -kksubs --project [project-directory] rename -``` -Renames the images in the `images` directory. Also renames the image keys in any draft. - -## Activate/Compose/Clear -```bash -kksubs --project [project-directory] compose -kksubs --project [project-directory] activate -kksubs --project [project-directory] clear -``` -Like `kkp`, `kksubs` is also equipped with `compose`, `activate` and `clear` commands, which serve the same purpose. Since there is no game directory, `activate` will not search for changes there. diff --git a/docs/command_line/koi.md b/docs/command_line/koi.md index c2613e4..74fcad2 100644 --- a/docs/command_line/koi.md +++ b/docs/command_line/koi.md @@ -1,72 +1,333 @@ # Koi Command Line Reference -Command line tool for `kksubs`. Application data is stored in `~\.kksubs`. +`koi` is the unified command-line tool for managing Koikatsu Party subtitle projects. Application data is stored in `~/.kksubs`. + +## Overview + +Think of `koi` as a save file manager for Koikatsu Party with subtitle automation: +- **Library**: Collection of all your projects (like save files or game cartridges) +- **Project**: A named collection of game UserData + subtitle files (a specific save/cartridge) +- **Active Project**: The project currently loaded in your game directory and workspace +- **Game Directory**: Koikatsu installation where projects are loaded (like a cartridge player) +- **Workspace**: Subtitle processing layer that works on captures from the active project + +When you "checkout" a project, it loads into both your game directory and workspace. The library stores backups of all projects. + +--- + +## Configuration + +### Configure +Configure application for the first time. Sets up game directory, library directory, and workspace directory. -## Configure -Configure application for the first time. ```sh koi configure ``` -## Archive -Archive (aka Project) related tools. -### List Archives -List archives. +Interactive prompts will ask for: +- **Game directory**: Path to Koikatsu Party installation (contains `UserData/`) +- **Library directory**: Where to store project backups +- **Workspace directory**: Where subtitle files live (drafts, images, output) + +### Info +Display current configuration and project information. + +```sh +koi info +``` + +Show recent projects: +```sh +koi info --recent +``` + +### Version +Get application version. + +```sh +koi version +``` + +--- + +## Project Management + +Projects are stored in the library and can be loaded into your game directory. Think of them as save files that can be swapped in and out. + +### List Projects +List all projects in the library. ```sh koi archive list ``` -### Create Archive -Create an archive. +Filter by pattern (supports wildcards): +```sh +koi archive list -p "story*" +koi archive list -p "genre/project-*" +``` + +Show recently accessed projects: +```sh +koi archive list --recent +``` + +### Create Project +Create a new project from the current game UserData state. + ```sh koi archive create ``` -### Remove Archive -Remove an archive. +Example: ```sh -koi archive remove +koi archive create my-first-story +koi archive create action/beach-episode +``` + +This saves: +- Current game `UserData/` to `library//UserData/` +- Current workspace state to `library//kksubs-project/` + +### Checkout Project +Load a project from the library into your game directory and workspace. + +```sh +koi checkout +``` + +Example: +```sh +koi checkout my-first-story +``` + +Create a new project from the current one (like `git checkout -b`): +```sh +koi checkout -b +``` + +Example workflow: +```sh +# Create branch from current project +koi checkout -b story-1 + +# Work on story-1, then create another branch +koi checkout -b story-2 + +# Switch back to story-1 +koi checkout story-1 ``` -### Rename Archive -Rename an archive. +### Rename Project +Rename the currently active project. + ```sh koi archive rename ``` -## Checkout -Switch to a different archive. +### Remove Project +Delete a project from the library. + ```sh -koi checkout [-b] +koi archive remove ``` -## Run -Run `kksubs` subtitling program. +**Warning**: This permanently deletes the project from the library. + +--- + +## Synchronization + +### Sync +Synchronize files between game directory, workspace, and library. + ```sh -koi run [--forever] +koi sync ``` -## Show -Open subtitled output images in File Explorer. +This performs: +- Bidirectional sync: `game/UserData` ↔ `library//UserData` +- Unidirectional sync: `game/UserData/cap` → `workspace/images` +- Bidirectional sync: `workspace/` ↔ `library//kksubs-project/` + +See [Architecture](../architecture.md) for detailed synchronization flow. + +--- + +## Subtitle Operations + +### Run +Execute subtitle processing on workspace images. + +One-time run (sync + generate subtitles once): +```sh +koi run +``` + +Watch mode (continuously watch for changes and regenerate): +```sh +koi run --forever +``` + +In watch mode, subtitles automatically regenerate when: +- Draft files are modified +- Style file is changed +- New captures appear in game directory +- Images are deleted + +Press Ctrl+C to stop watch mode. + +### Show +Open the subtitled output folders in your system file explorer. + ```sh koi show ``` -## Sync -Sync with archive +### Clear +Delete all generated subtitle outputs from the workspace. + ```sh -koi sync +koi clear +``` + +This removes all folders in `workspace/output/` (with confirmation prompt). Useful for starting fresh or troubleshooting. + +--- + +## Advanced Operations + +### Merge +Merge UserData content from another project into the currently loaded game directory. + +```sh +koi merge ``` -## Export -Export project galleries to a folder. +Example: +```sh +koi merge story-2 +``` + +This copies the following folders from the library project into your current game: +- `UserData/bg` (backgrounds) +- `UserData/cardframe` +- `UserData/chara` (characters) +- `UserData/coordinate` (outfits) +- `UserData/MaterialEditor` +- `UserData/Overlays` +- `UserData/studio` (scenes) + +**Note**: Screenshots (`cap`) and save files are NOT merged. Files with the same name will be overridden. + +**Use case**: Combine characters or scenes from different project branches. + +Supports wildcards: +```sh +koi merge "story-*" +``` + +### Export +Export project galleries (captures and subtitled outputs) to a destination folder. + ```sh koi export ``` -## Version -Get application version. +Exports: +- `UserData/cap` → `/cap` +- `kksubs-project/output` → `/output` + +Destination is configured in `~/.kksubs/config.yaml` under `settings.export.destination`. + +--- + +## Migrating from kkp + +If you previously used the deprecated `kkp` CLI, here's the command mapping: + +| Old kkp Command | New koi Command | Notes | +|-----------------|-----------------|-------| +| `kkp configure` | `koi configure` | Same | +| `kkp create ` | `koi archive create ` | Same functionality | +| `kkp list` | `koi archive list` | Same | +| `kkp checkout ` | `koi checkout ` | Same | +| `kkp checkout --branch ` | `koi checkout -b ` | Shorter flag | +| `kkp sync` | `koi sync` | Same | +| `kkp delete ` | `koi archive remove ` | Renamed to "remove" | +| `kkp compose` | `koi run` | Renamed to "run" | +| `kkp activate` | `koi run --forever` | Renamed to "run --forever" | +| `kkp show` | `koi show` | Same | +| `kkp clear` | `koi clear` | Same | +| `kkp merge ` | `koi merge ` | Same | +| `kkp export` | `koi export` | Same | +| `kkp game` | *(removed)* | Use scripts in `scripts/` directory | +| `kkp studio` | *(removed)* | Use scripts in `scripts/` directory | +| `kkp game-folder` | *(removed)* | Use scripts in `scripts/` directory | + +Game launcher commands were removed. Use the shell scripts in `scripts/windows/` or `scripts/xdg/` instead by adding them to your PATH. + +--- + +## Common Workflows + +### Starting a New Story ```sh -koi version -``` \ No newline at end of file +# Create project from current game state +koi archive create my-story + +# Work on subtitles +koi run --forever +``` + +### Creating Story Branches +```sh +# Create main story +koi archive create main-story + +# Create branch for alternate ending +koi checkout -b alternate-ending + +# Work on alternate ending... +# Switch back to main story +koi checkout main-story +``` + +### Merging Characters Between Projects +```sh +# You're working on story-1 +koi checkout story-1 + +# Merge characters from story-2 into current game +koi merge story-2 + +# Create new merged project +koi archive create story-1-merged +``` + +--- + +## Configuration File + +Configuration is stored in `~/.kksubs/config.yaml`: + +```yaml +game-directory: C:\Games\Koikatsu Party +library-directory: C:\Users\...\library +workspace-directory: C:\Users\...\workspace +settings: + export: + destination: C:\Users\...\exports + log: + level: warning +``` + +Metadata is stored in `~/.kksubs/metadata/data.yaml`: + +```yaml +current-project: genre/my-story +subtitle-sync-state: {...} +studio-sync-state: {...} +recent-projects: [...] +version: 3.2.0-dev +``` diff --git a/src/koi/cli.py b/src/koi/cli.py index 51f4730..e34b85f 100644 --- a/src/koi/cli.py +++ b/src/koi/cli.py @@ -68,6 +68,13 @@ def cli(): # Show subparsers.add_parser("show", help="Show subtitled images") + # Clear + subparsers.add_parser("clear", help="Clear all subtitled outputs") + + # Merge + merge_parser = subparsers.add_parser("merge", help="Merge UserData from another project into current game") + merge_parser.add_argument("project_id", type=str, help="Project ID or pattern") + args = parser.parse_args() command = args.command @@ -129,5 +136,12 @@ def cli(): if command == "export": controller.export_gallery() pass + if command == "clear": + controller.clear() + pass + if command == "merge": + project_id = args.project_id + controller.merge_project(project_id) + pass controller.close() From 1db4d73ac0a08508ad262a34a6c34a90641d391e Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:49:20 -0800 Subject: [PATCH 08/13] implement character/dialogue dual subtitling as first class --- src/kksubs/data/subtitle/style.py | 18 +- src/kksubs/data/subtitle/style_attributes.py | 104 ++++++- src/kksubs/data/subtitle/subtitle.py | 20 +- src/kksubs/service/extraction/subtitle.py | 50 +++- src/kksubs/service/subtitle.py | 215 +++++++++++++- .../test_character_dialogue_config.py | 161 ++++++++++ .../test_character_dialogue_integration.py | 156 ++++++++++ .../test_character_dialogue_parsing.py | 281 ++++++++++++++++++ 8 files changed, 986 insertions(+), 19 deletions(-) create mode 100644 tests/test_kksubs/test_character_dialogue_config.py create mode 100644 tests/test_kksubs/test_character_dialogue_integration.py create mode 100644 tests/test_kksubs/test_character_dialogue_parsing.py diff --git a/src/kksubs/data/subtitle/style.py b/src/kksubs/data/subtitle/style.py index 8a17e64..c427727 100644 --- a/src/kksubs/data/subtitle/style.py +++ b/src/kksubs/data/subtitle/style.py @@ -5,16 +5,16 @@ from common.data.representable import RepresentableData from common.utils.coalesce import coalesce from kksubs.data.subtitle.style_attributes import ( - Asset, Background, BaseData, BoxData, Brightness, Gaussian, - Mask, Motion, OutlineData, OutlineData1, TextData + Asset, Background, BaseData, BoxData, Brightness, CharacterDialogueConfig, + Gaussian, Mask, Motion, OutlineData, OutlineData1, TextData ) class Style(BaseData): field_name = "style" def __init__( - self, - style_id:str=None, + self, + style_id:str=None, text_data:TextData=None, outline_data:OutlineData=None, outline_data_1:OutlineData1=None, @@ -25,6 +25,7 @@ def __init__( motion:Motion=None, background:Background=None, mask:Mask=None, + character_dialogue:CharacterDialogueConfig=None, styles:List["Style"]=None, ): if text_data is None: @@ -43,6 +44,7 @@ def __init__( self.motion = motion self.background = background self.mask = mask + self.character_dialogue = character_dialogue self.styles = styles pass @@ -74,6 +76,7 @@ def deserialize(cls, style_dict:dict=None): motion=Motion.deserialize(values=style_dict.get(Motion.field_name)), background=Background.deserialize(path=style_dict.get(Background.field_name)), mask=Background.deserialize(path=style_dict.get(Mask.field_name)), + character_dialogue=CharacterDialogueConfig.deserialize(data=style_dict.get(CharacterDialogueConfig.field_name)), styles=styles, ) @@ -124,6 +127,10 @@ def coalesce(self, other:"Style", essential=False): self.mask = other.mask else: self.mask.coalesce(other.mask) + if self.character_dialogue is None: + self.character_dialogue = other.character_dialogue + else: + self.character_dialogue.coalesce(other.character_dialogue) if not self.styles: self.styles = other.styles @@ -158,6 +165,9 @@ def correct_values(self): if self.mask is not None: self.mask.coalesce(Mask.get_default()) self.mask.correct_values() + if self.character_dialogue is not None: + self.character_dialogue.coalesce(CharacterDialogueConfig.get_default()) + self.character_dialogue.correct_values() if self.styles is not None: for style in self.styles: style.correct_values() diff --git a/src/kksubs/data/subtitle/style_attributes.py b/src/kksubs/data/subtitle/style_attributes.py index f25492b..1db498d 100644 --- a/src/kksubs/data/subtitle/style_attributes.py +++ b/src/kksubs/data/subtitle/style_attributes.py @@ -354,13 +354,13 @@ def __init__( def get_default(cls): return Background( ) - + @classmethod def deserialize(cls, path=None): if path is None: return None return Background(**path) - + def coalesce(self, other:"Background"): if other is None: return @@ -370,3 +370,103 @@ def correct_values(self): return pass + +class CharacterDialogueConfig(BaseData): + field_name = "character_dialogue" + + def __init__( + self, + enabled=None, + character_name=None, + separator=None, + spacing=None, + character=None, + dialogue=None, + ): + self.enabled = enabled + self.character_name = character_name + self.separator = separator + self.spacing = spacing + self.character = character + self.dialogue = dialogue + pass + + @classmethod + def get_default(cls): + return CharacterDialogueConfig( + enabled=False, + character_name="", + separator=": ", + spacing=0, + character=None, + dialogue=None, + ) + + @classmethod + def deserialize(cls, data=None): + if data is None: + return None + + from kksubs.data.subtitle.style import Style + + enabled = data.get('enabled', False) + character_name = data.get('character_name', "") + separator = data.get('separator', ": ") + spacing = data.get('spacing', 0) + + character_data = data.get('character') + dialogue_data = data.get('dialogue') + + character = Style.deserialize(character_data) if character_data is not None else None + dialogue = Style.deserialize(dialogue_data) if dialogue_data is not None else None + + return CharacterDialogueConfig( + enabled=enabled, + character_name=character_name, + separator=separator, + spacing=spacing, + character=character, + dialogue=dialogue, + ) + + def coalesce(self, other:"CharacterDialogueConfig"): + if other is None: + return + + self.enabled = coalesce(self.enabled, other.enabled) + self.character_name = coalesce(self.character_name, other.character_name) + self.separator = coalesce(self.separator, other.separator) + self.spacing = coalesce(self.spacing, other.spacing) + + if self.character is None: + self.character = other.character + elif other.character is not None: + self.character.coalesce(other.character) + + if self.dialogue is None: + self.dialogue = other.dialogue + elif other.dialogue is not None: + self.dialogue.coalesce(other.dialogue) + + def correct_values(self): + if self.enabled is not None: + assert isinstance(self.enabled, bool) + + if self.character_name is not None: + self.character_name = to_string(self.character_name) + + if self.separator is not None: + self.separator = to_string(self.separator) + + self.spacing = to_integer(self.spacing) + + if self.enabled and not self.character_name: + raise ValueError("character_name is required when character_dialogue is enabled") + + if self.character is not None: + self.character.correct_values() + + if self.dialogue is not None: + self.dialogue.correct_values() + + pass diff --git a/src/kksubs/data/subtitle/subtitle.py b/src/kksubs/data/subtitle/subtitle.py index dbacf32..0ee6afc 100644 --- a/src/kksubs/data/subtitle/subtitle.py +++ b/src/kksubs/data/subtitle/subtitle.py @@ -21,9 +21,27 @@ def __init__(self, content:List[str]=None, style:Style=None): def deserialize(self, subtitle_data:Dict) -> "Subtitle": if subtitle_data is None: return None - + return Subtitle(content=subtitle_data.get('content'), style=Style.deserialize(subtitle_data.get('style'))) +class CharacterDialogueSubtitle(Subtitle): + + def __init__(self, dialogue_content:List[str]=None, style:Style=None): + super().__init__(content=[], style=style) + if dialogue_content is None: + dialogue_content = [] + self.dialogue_content = dialogue_content + + @classmethod + def deserialize(self, subtitle_data:Dict) -> "CharacterDialogueSubtitle": + if subtitle_data is None: + return None + + return CharacterDialogueSubtitle( + dialogue_content=subtitle_data.get('dialogue_content'), + style=Style.deserialize(subtitle_data.get('style')) + ) + class SubtitleGroup(RepresentableData): def __init__(self, image_id:str=None, input_image_path:str=None, image_modified_time=None, output_image_path:str=None, subtitles:List[Subtitle]=None): diff --git a/src/kksubs/service/extraction/subtitle.py b/src/kksubs/service/extraction/subtitle.py index 81732e7..f8b5146 100644 --- a/src/kksubs/service/extraction/subtitle.py +++ b/src/kksubs/service/extraction/subtitle.py @@ -3,11 +3,11 @@ from copy import deepcopy from kksubs.data.subtitle.style_attributes import ( - Asset, Background, BaseData, BoxData, Brightness, Gaussian, - Mask, Motion, OutlineData, OutlineData1, TextData + Asset, Background, BaseData, BoxData, Brightness, CharacterDialogueConfig, + Gaussian, Mask, Motion, OutlineData, OutlineData1, TextData ) from kksubs.data.subtitle.style import Style -from kksubs.data.subtitle.subtitle import Subtitle, SubtitleGroup +from kksubs.data.subtitle.subtitle import CharacterDialogueSubtitle, Subtitle, SubtitleGroup # from kksubs.data.subtitle.subtitle import Background, BaseData, BoxData, Brightness, Gaussian, Mask, Motion, OutlineData, OutlineData1, Style, Subtitle, SubtitleGroup, TextData # parsing/extraction, filtering, standardization @@ -15,20 +15,30 @@ default_style_by_field_name:Dict[str, BaseData] = { base_data.field_name:base_data for base_data in [ - TextData, - OutlineData, - OutlineData1, - BoxData, + TextData, + OutlineData, + OutlineData1, + BoxData, Asset, - Brightness, - Gaussian, - Motion, - Background, + Brightness, + Gaussian, + Motion, + Background, Mask, + CharacterDialogueConfig, Style ] } +def _is_character_dialogue_style(style_id: str, styles: Dict[str, Style]) -> bool: + """Check if a style has character_dialogue enabled.""" + style = styles.get(style_id) + return ( + style is not None + and style.character_dialogue is not None + and style.character_dialogue.enabled + ) + def _is_valid_nested_attribute(style:BaseData, nested_attribute:str) -> bool: attributes = nested_attribute.split(".") if len(attributes) == 1: @@ -123,6 +133,19 @@ def get_has_attr_key(line:str): key, line_content = lsplit if key == "content": pass + elif _is_character_dialogue_style(key, styles): + # Character/dialogue subtitle + if is_start_of_subtitle: + # Create CharacterDialogueSubtitle instead of regular Subtitle + style = deepcopy(styles.get(key)) + dialogue_content = [] + subtitle = CharacterDialogueSubtitle(dialogue_content=dialogue_content, style=style) + # Replace the last subtitle that was just created + subtitles[-1] = subtitle + # Append dialogue content + if line_content.lstrip(): + subtitle.dialogue_content.append(line_content.lstrip()) + continue else: # apply alias as style. # deep copy to enforce independence between subtitle objects, esp. for child styles. style.coalesce(deepcopy(styles.get(key))) @@ -132,6 +155,11 @@ def get_has_attr_key(line:str): line_content = lsplit[1].lstrip() if len(lsplit) == 2 else "" else: line_content = line + # If we're in a character/dialogue subtitle, append to dialogue_content + if isinstance(subtitle, CharacterDialogueSubtitle): + if line_content.strip(): + subtitle.dialogue_content.append(line_content) + continue if not line_content: empty_lines.append(line_content) else: diff --git a/src/kksubs/service/subtitle.py b/src/kksubs/service/subtitle.py index c3aac2d..ef0d573 100644 --- a/src/kksubs/service/subtitle.py +++ b/src/kksubs/service/subtitle.py @@ -5,7 +5,7 @@ from common.utils.coalesce import coalesce from kksubs.data.subtitle.style_attributes import OutlineData -from kksubs.data.subtitle.subtitle import Subtitle +from kksubs.data.subtitle.subtitle import CharacterDialogueSubtitle, Subtitle # from kksubs.data.subtitle.subtitle import OutlineData, Subtitle from kksubs.service.processor.motion_blur import apply_motion_blur from kksubs.service.processor.apply_text import create_text_layer @@ -47,8 +47,221 @@ def _get_default_font(): # If importlib.resources fails, return None for system default return None +def _render_text_with_outlines( + image: Image.Image, + content: List[str], + style, + font: ImageFont.FreeTypeFont, + tb_anchor_x: float, + tb_anchor_y: float, + tb_center_x: float, + tb_center_y: float, +) -> Image.Image: + """ + Helper to render text with outline layers (outline_data_1, outline_data). + Returns a layer with text and outlines composited. + """ + text_data = style.text_data + box_data = style.box_data + + font_color = text_data.color + font_size = text_data.size + font_stroke_size = text_data.stroke_size + font_stroke_color = text_data.stroke_color + align_h = box_data.align_h + align_v = box_data.align_v + box_width = box_data.box_width + rotate = box_data.rotate if box_data.rotate is not None else 0 + + # Create base layer + layer = Image.new("RGBA", image.size, (0, 0, 0, 0)) + + # Create text layer + text_layer = create_text_layer( + layer, font, content, font_color, font_size, + font_stroke_color, font_stroke_size, align_h, align_v, + box_width, tb_anchor_x, tb_anchor_y + ).rotate(rotate, center=(tb_center_x, tb_center_y)) + + # Apply outline layers + for outline_data in [style.outline_data_1, style.outline_data]: + if outline_data is not None and isinstance(outline_data, OutlineData): + outline_color = outline_data.color + outline_size = outline_data.size + outline_blur = outline_data.blur + outline_alpha = outline_data.alpha + try: + outline_layer = create_text_layer( + layer, font, content, outline_color, font_size, + outline_color, outline_size, align_h, align_v, + box_width, tb_anchor_x, tb_anchor_y + ).rotate(rotate, center=(tb_center_x, tb_center_y)) + outline_base = outline_layer + if outline_blur is not None and isinstance(outline_blur, int) and outline_blur > 0: + outline_base = layer.copy() + outline_base.paste(outline_layer, (0, 0), outline_layer) + outline_base = outline_base.filter(ImageFilter.GaussianBlur(radius=outline_blur)) + outline_layer = outline_layer.filter(ImageFilter.GaussianBlur(radius=outline_blur)).convert("RGBA") + if outline_alpha is not None and outline_alpha < 1: + outline_layer = ImageEnhance.Brightness(outline_layer.getchannel('A')).enhance(outline_alpha) + layer.paste(outline_base, (0, 0), outline_layer) + except Exception as e: + logger.warning(f'Failed to process outline: {e}') + + # Apply text layer + text_mask = text_layer + if text_data.alpha is not None and text_data.alpha < 1: + text_mask = ImageEnhance.Brightness(text_mask.getchannel('A')).enhance(text_data.alpha) + layer.paste(text_layer, (0, 0), text_mask) + + return layer + +def _render_character_dialogue_subtitle( + image: Image.Image, + subtitle: CharacterDialogueSubtitle, + project_directory: str +) -> Image.Image: + """ + Render character/dialogue subtitle as horizontally-arranged container. + + Character and dialogue are rendered as separate elements with independent + styling, then positioned as a single container unit. + """ + style = subtitle.style + config = style.character_dialogue + + if config is None or not config.enabled: + logger.warning("CharacterDialogueSubtitle has no valid character_dialogue config, rendering as empty.") + return image + + # Get character name and dialogue + char_text = config.character_name + separator_text = config.separator if config.separator is not None else ": " + spacing = config.spacing if config.spacing is not None else 0 + dialogue_lines = subtitle.dialogue_content if subtitle.dialogue_content else [""] + + # Get default styles if not provided + char_style = config.character if config.character is not None else style + dialogue_style = config.dialogue if config.dialogue is not None else style + + # Ensure styles have defaults coalesced + if char_style.box_data.box_width is None: + from kksubs.data.subtitle.style_attributes import BoxData + char_style.box_data.coalesce(BoxData.get_default()) + if dialogue_style.box_data.box_width is None: + from kksubs.data.subtitle.style_attributes import BoxData + dialogue_style.box_data.coalesce(BoxData.get_default()) + + # Get character font + char_text_data = char_style.text_data + if char_text_data.font == "default": + char_font_path = _get_default_font() + elif char_text_data.font and os.path.exists(char_text_data.font): + char_font_path = char_text_data.font + else: + logger.warning(f"Cannot find character font {char_text_data.font}, using default.") + char_font_path = _get_default_font() + + try: + char_font = ImageFont.truetype(char_font_path, char_text_data.size) + except Exception as e: + logger.warning(f"Failed to load character font: {e}") + return image + + # Get dialogue font + dialogue_text_data = dialogue_style.text_data + if dialogue_text_data.font == "default": + dialogue_font_path = _get_default_font() + elif dialogue_text_data.font and os.path.exists(dialogue_text_data.font): + dialogue_font_path = dialogue_text_data.font + else: + logger.warning(f"Cannot find dialogue font {dialogue_text_data.font}, using default.") + dialogue_font_path = _get_default_font() + + try: + dialogue_font = ImageFont.truetype(dialogue_font_path, dialogue_text_data.size) + except Exception as e: + logger.warning(f"Failed to load dialogue font: {e}") + return image + + # Calculate text dimensions + char_bbox = char_font.getbbox(char_text) + char_width = char_bbox[2] - char_bbox[0] + char_height = char_bbox[3] - char_bbox[1] + + sep_bbox = char_font.getbbox(separator_text) + sep_width = sep_bbox[2] - sep_bbox[0] + sep_height = sep_bbox[3] - sep_bbox[1] + + # Calculate dialogue dimensions + dialogue_text = "\n".join(dialogue_lines) + dialogue_bbox = dialogue_font.getbbox(dialogue_text) + dialogue_width = dialogue_bbox[2] - dialogue_bbox[0] + dialogue_height = dialogue_bbox[3] - dialogue_bbox[1] + + # Calculate container dimensions (horizontal layout) + container_width = char_width + sep_width + spacing + dialogue_width + container_height = max(char_height, sep_height, dialogue_height) + + # Get container anchor point + box_data = style.box_data + tb_center_x, tb_center_y = get_pil_coordinates( + image, anchor=box_data.anchor, grid4=box_data.grid4, + grid10=box_data.grid10, nudge=None + ) + tb_anchor_x, tb_anchor_y = get_pil_coordinates( + image, anchor=box_data.anchor, grid4=box_data.grid4, + grid10=box_data.grid10, nudge=box_data.nudge + ) + + # Calculate container top-left based on alignment + if box_data.align_h == "center": + container_x = tb_anchor_x - container_width / 2 + elif box_data.align_h == "left": + container_x = tb_anchor_x + else: # right + container_x = tb_anchor_x - container_width + + if box_data.align_v == "center": + container_y = tb_anchor_y - container_height / 2 + elif box_data.align_v == "top": + container_y = tb_anchor_y + else: # bottom + container_y = tb_anchor_y - container_height + + # Render character portion + char_layer = _render_text_with_outlines( + image, [char_text], char_style, char_font, + container_x, container_y, tb_center_x, tb_center_y + ) + + # Render separator portion (uses character style) + sep_layer = _render_text_with_outlines( + image, [separator_text], char_style, char_font, + container_x + char_width, container_y, tb_center_x, tb_center_y + ) + + # Render dialogue portion + dialogue_layer = _render_text_with_outlines( + image, dialogue_lines, dialogue_style, dialogue_font, + container_x + char_width + sep_width + spacing, container_y, + tb_center_x, tb_center_y + ) + + # Composite all layers + image = image.convert("RGBA") + image = Image.alpha_composite(image, char_layer) + image = Image.alpha_composite(image, sep_layer) + image = Image.alpha_composite(image, dialogue_layer) + + return image + def add_subtitle_to_image(image:Image.Image, subtitle:Subtitle, project_directory:str) -> Image.Image: + # Check if this is a character/dialogue subtitle + if isinstance(subtitle, CharacterDialogueSubtitle): + return _render_character_dialogue_subtitle(image, subtitle, project_directory) + # expand subtitle. style = subtitle.style content = subtitle.content diff --git a/tests/test_kksubs/test_character_dialogue_config.py b/tests/test_kksubs/test_character_dialogue_config.py new file mode 100644 index 0000000..4bf00fb --- /dev/null +++ b/tests/test_kksubs/test_character_dialogue_config.py @@ -0,0 +1,161 @@ +import pytest +from kksubs.data.subtitle.style_attributes import CharacterDialogueConfig +from kksubs.data.subtitle.style import Style + + +def test_character_dialogue_config_deserialize_basic(): + """Test basic deserialization of CharacterDialogueConfig.""" + data = { + 'enabled': True, + 'character_name': 'Shakespeare', + 'separator': ': ', + 'spacing': 0, + } + config = CharacterDialogueConfig.deserialize(data) + assert config.enabled is True + assert config.character_name == 'Shakespeare' + assert config.separator == ': ' + assert config.spacing == 0 + assert config.character is None + assert config.dialogue is None + + +def test_character_dialogue_config_deserialize_with_nested_styles(): + """Test deserialization with nested character and dialogue styles.""" + data = { + 'enabled': True, + 'character_name': 'Romeo', + 'separator': ': ', + 'spacing': 5, + 'character': { + 'text_data': { + 'color': [255, 215, 0], + 'size': 24, + }, + }, + 'dialogue': { + 'text_data': { + 'color': [255, 255, 255], + 'size': 20, + }, + }, + } + config = CharacterDialogueConfig.deserialize(data) + assert config.enabled is True + assert config.character_name == 'Romeo' + assert config.spacing == 5 + assert config.character is not None + assert config.character.text_data.color == [255, 215, 0] + assert config.character.text_data.size == 24 + assert config.dialogue is not None + assert config.dialogue.text_data.color == [255, 255, 255] + assert config.dialogue.text_data.size == 20 + + +def test_character_dialogue_config_deserialize_none(): + """Test that deserialize returns None when data is None.""" + config = CharacterDialogueConfig.deserialize(None) + assert config is None + + +def test_character_dialogue_config_get_default(): + """Test get_default returns correct default values.""" + config = CharacterDialogueConfig.get_default() + assert config.enabled is False + assert config.character_name == "" + assert config.separator == ": " + assert config.spacing == 0 + assert config.character is None + assert config.dialogue is None + + +def test_character_dialogue_config_coalesce(): + """Test coalescing two CharacterDialogueConfig objects.""" + config1 = CharacterDialogueConfig( + enabled=True, + character_name='Hamlet', + separator=': ', + spacing=10, + ) + config2 = CharacterDialogueConfig( + enabled=False, + character_name='Ophelia', + separator=' - ', + spacing=5, + ) + config1.coalesce(config2) + # config1 values should be preserved (not overwritten by config2) + assert config1.enabled is True + assert config1.character_name == 'Hamlet' + assert config1.separator == ': ' + assert config1.spacing == 10 + + +def test_character_dialogue_config_coalesce_with_nested_styles(): + """Test coalescing with nested character and dialogue styles.""" + char_style1 = Style.deserialize({ + 'text_data': {'color': [255, 0, 0], 'size': 24} + }) + char_style2 = Style.deserialize({ + 'text_data': {'color': [0, 255, 0], 'size': 30}, + 'outline_data': {'color': [0, 0, 0], 'size': 2} + }) + + config1 = CharacterDialogueConfig( + enabled=True, + character_name='Juliet', + character=char_style1, + ) + config2 = CharacterDialogueConfig( + enabled=True, + character_name='Romeo', + character=char_style2, + ) + + config1.coalesce(config2) + # config1 character style should be merged with config2's + assert config1.character.text_data.color == [255, 0, 0] # from config1 + assert config1.character.text_data.size == 24 # from config1 + assert config1.character.outline_data.color == [0, 0, 0] # from config2 + assert config1.character.outline_data.size == 2 # from config2 + + +def test_character_dialogue_config_correct_values(): + """Test correct_values validates and transforms values.""" + config = CharacterDialogueConfig( + enabled=True, + character_name='Macbeth', + separator=': ', + spacing='10', # String that should be converted to int + ) + config.correct_values() + assert config.character_name == 'Macbeth' + assert config.spacing == 10 # Should be converted to int + + +def test_character_dialogue_config_correct_values_missing_character_name(): + """Test that correct_values raises error when enabled but no character_name.""" + config = CharacterDialogueConfig( + enabled=True, + character_name='', # Empty character name + ) + with pytest.raises(ValueError, match="character_name is required"): + config.correct_values() + + +def test_character_dialogue_config_correct_values_with_nested_styles(): + """Test that correct_values recursively calls correct_values on nested styles.""" + config = CharacterDialogueConfig( + enabled=True, + character_name='Othello', + character=Style.deserialize({ + 'text_data': {'color': 'red', 'size': '24'} # String values + }), + dialogue=Style.deserialize({ + 'text_data': {'color': 'white', 'size': '20'} # String values + }), + ) + config.correct_values() + # After correct_values, nested styles should have corrected types + assert config.character.text_data.size == 24 # Converted to int + assert config.dialogue.text_data.size == 20 # Converted to int diff --git a/tests/test_kksubs/test_character_dialogue_integration.py b/tests/test_kksubs/test_character_dialogue_integration.py new file mode 100644 index 0000000..31e52d3 --- /dev/null +++ b/tests/test_kksubs/test_character_dialogue_integration.py @@ -0,0 +1,156 @@ +"""Integration tests for character/dialogue rendering workflow.""" +import tempfile +import os +from PIL import Image +from kksubs.controller.subtitle import SubtitleController + + +def generate_test_image(directory, image_name): + """Generate a test image in the specified directory.""" + image = Image.new('RGB', (1920, 1080), (255, 255, 255)) + image_path = os.path.join(directory, image_name) + image.save(image_path) + image.close() + + +def test_character_dialogue_rendering_workflow(): + """Test end-to-end character/dialogue subtitle rendering.""" + with tempfile.TemporaryDirectory() as test_dir: + # Create controller + controller = SubtitleController(test_dir) + controller.create() + + # Generate test image + image_dir = controller.get_image_directory() + generate_test_image(image_dir, 'scene1.png') + + # Create styles.yml with character/dialogue style + styles_content = """ +- style_id: shakespeare + character_dialogue: + enabled: true + character_name: "Shakespeare" + separator: ": " + character: + text_data: + color: [255, 215, 0] + size: 24 + outline_data: + color: [0, 0, 0] + size: 2 + dialogue: + text_data: + color: [255, 255, 255] + size: 20 + outline_data: + color: [0, 0, 0] + size: 2 + box_data: + grid10: [5, 5] + align_h: center + align_v: center + +- style_id: narrator + text_data: + color: [200, 200, 200] + size: 18 + box_data: + grid10: [5, 1] + align_h: center + align_v: top +""" + styles_path = os.path.join(controller.workspace_directory, 'styles.yml') + with open(styles_path, 'w') as f: + f.write(styles_content) + + # Create draft with character/dialogue + scripts_dir = controller.get_scripts_directory() + draft_files = os.listdir(scripts_dir) + assert len(draft_files) == 1 + draft_path = os.path.join(scripts_dir, draft_files[0]) + + draft_content = """ +image_id: scene1.png +narrator: Once upon a time... +shakespeare: To be or not to be, +that is the question +""" + with open(draft_path, 'w') as f: + f.write(draft_content) + + # Run subtitle generation + controller.add_subtitles(allow_multiprocessing=False) + + # Verify output + output_dir = controller.get_output_directory_by_script(draft_files[0]) + assert os.path.exists(output_dir) + output_files = os.listdir(output_dir) + assert 'scene1.png' in output_files + + # Verify output image exists and has reasonable size + output_image_path = os.path.join(output_dir, 'scene1.png') + assert os.path.exists(output_image_path) + output_image = Image.open(output_image_path) + assert output_image.size == (1920, 1080) + output_image.close() + + +def test_mixed_subtitle_types_workflow(): + """Test workflow with both regular and character/dialogue subtitles.""" + with tempfile.TemporaryDirectory() as test_dir: + controller = SubtitleController(test_dir) + controller.create() + + # Generate test image + image_dir = controller.get_image_directory() + generate_test_image(image_dir, 'scene2.png') + + # Create styles.yml + styles_content = """ +- style_id: character1 + character_dialogue: + enabled: true + character_name: "Alice" + character: + text_data: + color: [255, 100, 100] + size: 22 + dialogue: + text_data: + color: [255, 255, 255] + size: 18 + +- style_id: regular_style + text_data: + color: [150, 150, 150] + size: 16 +""" + styles_path = os.path.join(controller.workspace_directory, 'styles.yml') + with open(styles_path, 'w') as f: + f.write(styles_content) + + # Create draft with mixed subtitle types + scripts_dir = controller.get_scripts_directory() + draft_files = os.listdir(scripts_dir) + draft_path = os.path.join(scripts_dir, draft_files[0]) + + draft_content = """ +image_id: scene2.png +content: (A regular subtitle appears) +character1: Hello, world! +""" + with open(draft_path, 'w') as f: + f.write(draft_content) + + # Run subtitle generation + controller.add_subtitles(allow_multiprocessing=False) + + # Verify output + output_dir = controller.get_output_directory_by_script(draft_files[0]) + output_image_path = os.path.join(output_dir, 'scene2.png') + assert os.path.exists(output_image_path) + + # Verify image can be opened (basic validation) + output_image = Image.open(output_image_path) + assert output_image.size == (1920, 1080) + output_image.close() diff --git a/tests/test_kksubs/test_character_dialogue_parsing.py b/tests/test_kksubs/test_character_dialogue_parsing.py new file mode 100644 index 0000000..6e38383 --- /dev/null +++ b/tests/test_kksubs/test_character_dialogue_parsing.py @@ -0,0 +1,281 @@ +import pytest +import tempfile +import os +from PIL import Image +from kksubs.service.extraction.subtitle import extract_subtitle_groups +from kksubs.service.extraction.style import extract_styles +from kksubs.data.subtitle.subtitle import CharacterDialogueSubtitle, Subtitle + + +@pytest.fixture +def temp_image_dir(): + """Create a temporary directory with a test image.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create a test image + image = Image.new('RGB', (800, 600), (255, 255, 255)) + image_path = os.path.join(temp_dir, 'test.png') + image.save(image_path) + image.close() + yield temp_dir + + +def test_parse_character_dialogue_basic(temp_image_dir): + """Test parsing basic character/dialogue syntax.""" + styles_yaml = [ + { + 'style_id': 'shakespeare', + 'character_dialogue': { + 'enabled': True, + 'character_name': 'Shakespeare', + 'separator': ': ', + 'character': { + 'text_data': {'color': [255, 215, 0], 'size': 24} + }, + 'dialogue': { + 'text_data': {'color': [255, 255, 255], 'size': 20} + } + }, + 'box_data': { + 'grid10': [5, 5], + 'align_h': 'center', + 'align_v': 'center' + } + } + ] + styles = extract_styles(styles_yaml) + + draft = """ +image_id: test.png +shakespeare: to be or not to be +""" + + with tempfile.TemporaryDirectory() as output_dir: + subtitle_groups = extract_subtitle_groups( + draft_id='test_draft', + draft_body=draft, + styles=styles, + image_dir=temp_image_dir, + output_dir=output_dir + ) + + assert 'test.png' in subtitle_groups + subtitles = subtitle_groups['test.png'][0].subtitles + assert len(subtitles) == 1 + assert isinstance(subtitles[0], CharacterDialogueSubtitle) + assert subtitles[0].dialogue_content == ['to be or not to be'] + assert subtitles[0].style.character_dialogue.character_name == 'Shakespeare' + + +def test_parse_character_dialogue_multiline(temp_image_dir): + """Test parsing multi-line character/dialogue.""" + styles_yaml = [ + { + 'style_id': 'hamlet', + 'character_dialogue': { + 'enabled': True, + 'character_name': 'Hamlet', + 'character': {'text_data': {'color': [255, 215, 0]}}, + 'dialogue': {'text_data': {'color': [255, 255, 255]}} + } + } + ] + styles = extract_styles(styles_yaml) + + draft = """ +image_id: test.png +hamlet: to be or not to be, +that is the question +""" + + with tempfile.TemporaryDirectory() as output_dir: + subtitle_groups = extract_subtitle_groups( + draft_id='test_draft', + draft_body=draft, + styles=styles, + image_dir=temp_image_dir, + output_dir=output_dir + ) + + subtitles = subtitle_groups['test.png'][0].subtitles + assert len(subtitles) == 1 + assert isinstance(subtitles[0], CharacterDialogueSubtitle) + assert subtitles[0].dialogue_content == [ + 'to be or not to be,', + 'that is the question' + ] + + +def test_parse_mixed_regular_and_character_dialogue(temp_image_dir): + """Test parsing mixed regular and character/dialogue subtitles.""" + styles_yaml = [ + { + 'style_id': 'narrator', + 'text_data': {'color': [200, 200, 200], 'size': 16} + }, + { + 'style_id': 'romeo', + 'character_dialogue': { + 'enabled': True, + 'character_name': 'Romeo', + 'character': {'text_data': {'color': [255, 0, 0]}}, + 'dialogue': {'text_data': {'color': [255, 255, 255]}} + } + } + ] + styles = extract_styles(styles_yaml) + + draft = """ +image_id: test.png +content: A sad tale begins +romeo: wherefore art thou +""" + + with tempfile.TemporaryDirectory() as output_dir: + subtitle_groups = extract_subtitle_groups( + draft_id='test_draft', + draft_body=draft, + styles=styles, + image_dir=temp_image_dir, + output_dir=output_dir + ) + + subtitles = subtitle_groups['test.png'][0].subtitles + assert len(subtitles) == 2 + # First subtitle is regular + assert isinstance(subtitles[0], Subtitle) + assert not isinstance(subtitles[0], CharacterDialogueSubtitle) + assert subtitles[0].content == ['A sad tale begins'] + # Second subtitle is character/dialogue + assert isinstance(subtitles[1], CharacterDialogueSubtitle) + assert subtitles[1].dialogue_content == ['wherefore art thou'] + + +def test_parse_character_dialogue_with_matrix(temp_image_dir): + """Test parsing character/dialogue with matrix-generated style IDs.""" + # Check existing matrix tests to understand correct syntax + from kksubs.data.subtitle.style_row_enum import STYLE_ROW_ENUM + + # grid10_complete should already be defined in the enum + assert STYLE_ROW_ENUM.get('grid10_complete') is not None + + styles_yaml = [ + { + 'matrix': [ + { + 'row': { + 'row_id': 'grid10_complete' + } + }, + { + 'row': { + 'styles': [ + { + 'style_id': 'shakespeare', + 'character_dialogue': { + 'enabled': True, + 'character_name': 'Shakespeare', + } + } + ] + } + } + ] + } + ] + styles = extract_styles(styles_yaml) + + # Verify matrix generated styles with grid positions + # The style IDs use single digits (e.g., '55' not '5-5') + assert 'shakespeare-55' in styles + assert 'shakespeare-00' in styles + assert 'shakespeare-99' in styles + + draft = """ +image_id: test.png +shakespeare-55: hello world +""" + + with tempfile.TemporaryDirectory() as output_dir: + subtitle_groups = extract_subtitle_groups( + draft_id='test_draft', + draft_body=draft, + styles=styles, + image_dir=temp_image_dir, + output_dir=output_dir + ) + + subtitles = subtitle_groups['test.png'][0].subtitles + assert len(subtitles) == 1 + assert isinstance(subtitles[0], CharacterDialogueSubtitle) + assert subtitles[0].dialogue_content == ['hello world'] + # Check grid position (tuple, not list) + assert subtitles[0].style.box_data.grid10 == (5, 5) + + +def test_parse_empty_dialogue(temp_image_dir): + """Test parsing character/dialogue with empty dialogue.""" + styles_yaml = [ + { + 'style_id': 'juliet', + 'character_dialogue': { + 'enabled': True, + 'character_name': 'Juliet', + } + } + ] + styles = extract_styles(styles_yaml) + + draft = """ +image_id: test.png +juliet: +""" + + with tempfile.TemporaryDirectory() as output_dir: + subtitle_groups = extract_subtitle_groups( + draft_id='test_draft', + draft_body=draft, + styles=styles, + image_dir=temp_image_dir, + output_dir=output_dir + ) + + subtitles = subtitle_groups['test.png'][0].subtitles + assert len(subtitles) == 1 + assert isinstance(subtitles[0], CharacterDialogueSubtitle) + # Empty dialogue should result in empty list or list with empty string + assert subtitles[0].dialogue_content == [] or subtitles[0].dialogue_content == [''] + + +def test_character_dialogue_style_used_with_content_key(temp_image_dir): + """Test that using content: key with a character/dialogue style creates regular Subtitle.""" + styles_yaml = [ + { + 'style_id': 'hamlet', + 'character_dialogue': { + 'enabled': True, + 'character_name': 'Hamlet', + } + } + ] + styles = extract_styles(styles_yaml) + + draft = """ +image_id: test.png +content: some text +""" + + with tempfile.TemporaryDirectory() as output_dir: + subtitle_groups = extract_subtitle_groups( + draft_id='test_draft', + draft_body=draft, + styles=styles, + image_dir=temp_image_dir, + output_dir=output_dir + ) + + subtitles = subtitle_groups['test.png'][0].subtitles + assert len(subtitles) == 1 + # Should be regular Subtitle, not CharacterDialogueSubtitle + assert isinstance(subtitles[0], Subtitle) + assert not isinstance(subtitles[0], CharacterDialogueSubtitle) + assert subtitles[0].content == ['some text'] From f32c94408181145721b8d963192dd83909cf132e Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 17:24:03 -0800 Subject: [PATCH 09/13] fix style shared reference problem --- src/kksubs/data/subtitle/style.py | 28 +- src/kksubs/data/subtitle/style_attributes.py | 6 +- tests/test_kksubs/test_style_independence.py | 282 +++++++++++++++++++ 3 files changed, 300 insertions(+), 16 deletions(-) create mode 100644 tests/test_kksubs/test_style_independence.py diff --git a/src/kksubs/data/subtitle/style.py b/src/kksubs/data/subtitle/style.py index c427727..a5da726 100644 --- a/src/kksubs/data/subtitle/style.py +++ b/src/kksubs/data/subtitle/style.py @@ -75,7 +75,7 @@ def deserialize(cls, style_dict:dict=None): gaussian=Gaussian.deserialize(values=style_dict.get(Gaussian.field_name)), motion=Motion.deserialize(values=style_dict.get(Motion.field_name)), background=Background.deserialize(path=style_dict.get(Background.field_name)), - mask=Background.deserialize(path=style_dict.get(Mask.field_name)), + mask=Mask.deserialize(path=style_dict.get(Mask.field_name)), character_dialogue=CharacterDialogueConfig.deserialize(data=style_dict.get(CharacterDialogueConfig.field_name)), styles=styles, ) @@ -85,54 +85,54 @@ def coalesce(self, other:"Style", essential=False): return self.style_id = coalesce(self.style_id, other.style_id) if self.text_data is None: - self.text_data = other.text_data + self.text_data = deepcopy(other.text_data) if other.text_data is not None else None else: self.text_data.coalesce(other.text_data) if self.outline_data is None: - self.outline_data = other.outline_data + self.outline_data = deepcopy(other.outline_data) if other.outline_data is not None else None else: self.outline_data.coalesce(other.outline_data) if self.outline_data_1 is None: - self.outline_data_1 = other.outline_data_1 + self.outline_data_1 = deepcopy(other.outline_data_1) if other.outline_data_1 is not None else None else: self.outline_data_1.coalesce(other.outline_data_1) if self.box_data is None: - self.box_data = other.box_data + self.box_data = deepcopy(other.box_data) if other.box_data is not None else None else: self.box_data.coalesce(other.box_data) if essential: return - + if self.asset_data is None: - self.asset_data = other.asset_data + self.asset_data = deepcopy(other.asset_data) if other.asset_data is not None else None else: self.asset_data.coalesce(other.asset_data) if self.brightness is None: - self.brightness = other.brightness + self.brightness = deepcopy(other.brightness) if other.brightness is not None else None else: self.brightness.coalesce(other.brightness) if self.gaussian is None: - self.gaussian = other.gaussian + self.gaussian = deepcopy(other.gaussian) if other.gaussian is not None else None else: self.gaussian.coalesce(other.gaussian) if self.motion is None: - self.motion = other.motion + self.motion = deepcopy(other.motion) if other.motion is not None else None else: self.motion.coalesce(other.motion) if self.background is None: - self.background = other.background + self.background = deepcopy(other.background) if other.background is not None else None else: self.background.coalesce(other.background) if self.mask is None: - self.mask = other.mask + self.mask = deepcopy(other.mask) if other.mask is not None else None else: self.mask.coalesce(other.mask) if self.character_dialogue is None: - self.character_dialogue = other.character_dialogue + self.character_dialogue = deepcopy(other.character_dialogue) if other.character_dialogue is not None else None else: self.character_dialogue.coalesce(other.character_dialogue) if not self.styles: - self.styles = other.styles + self.styles = deepcopy(other.styles) if other.styles is not None else None def inherit(self, style:"Style"): # inherit from other styles. diff --git a/src/kksubs/data/subtitle/style_attributes.py b/src/kksubs/data/subtitle/style_attributes.py index 1db498d..3baee7b 100644 --- a/src/kksubs/data/subtitle/style_attributes.py +++ b/src/kksubs/data/subtitle/style_attributes.py @@ -1,3 +1,5 @@ +from copy import deepcopy + from kksubs.data.abstract import BaseData from common.utils.coalesce import coalesce from kksubs.utils.sanitizers import to_float, to_integer, to_rgb_color, to_string, to_validated_value, to_xy_coords @@ -439,12 +441,12 @@ def coalesce(self, other:"CharacterDialogueConfig"): self.spacing = coalesce(self.spacing, other.spacing) if self.character is None: - self.character = other.character + self.character = deepcopy(other.character) if other.character is not None else None elif other.character is not None: self.character.coalesce(other.character) if self.dialogue is None: - self.dialogue = other.dialogue + self.dialogue = deepcopy(other.dialogue) if other.dialogue is not None else None elif other.dialogue is not None: self.dialogue.coalesce(other.dialogue) diff --git a/tests/test_kksubs/test_style_independence.py b/tests/test_kksubs/test_style_independence.py new file mode 100644 index 0000000..67e0db9 --- /dev/null +++ b/tests/test_kksubs/test_style_independence.py @@ -0,0 +1,282 @@ +""" +Tests for style object independence during coalescing operations. + +Verifies that each subtitle maintains independent style objects after coalescing, +ensuring modifications to one style do not affect other styles or the original +style dictionary. +""" +import tempfile +import os +from copy import deepcopy +from PIL import Image + +from kksubs.data.subtitle.style import Style +from kksubs.data.subtitle.style_attributes import TextData, BoxData, CharacterDialogueConfig +from kksubs.service.extraction.subtitle import extract_subtitle_groups +from kksubs.service.extraction.style import extract_styles + + +def test_coalesce_creates_independent_objects(): + """ + When coalescing styles, each style should get independent copies of + nested objects, not shared references. + """ + # Create a default style with text_data + default_style = Style(text_data=TextData(color=(255, 255, 255), size=20)) + + # Create two subtitles with empty styles + sub1_style = Style() + sub2_style = Style() + + # Coalesce both with the default style + sub1_style.coalesce(default_style) + sub2_style.coalesce(default_style) + + # All three should have text_data + assert sub1_style.text_data is not None + assert sub2_style.text_data is not None + assert default_style.text_data is not None + + # Each should have independent text_data objects + assert sub1_style.text_data is not default_style.text_data, \ + "sub1 should have independent text_data, not share with default" + assert sub2_style.text_data is not default_style.text_data, \ + "sub2 should have independent text_data, not share with default" + assert sub1_style.text_data is not sub2_style.text_data, \ + "sub1 and sub2 should have independent text_data objects" + + # Mutating one should not affect the others + sub1_style.text_data.color = (255, 0, 0) # Change to RED + + assert default_style.text_data.color == (255, 255, 255), \ + "Mutating sub1 should not affect default style" + assert sub2_style.text_data.color == (255, 255, 255), \ + "Mutating sub1 should not affect sub2" + + +def test_correct_values_does_not_mutate_shared_objects(): + """ + When correct_values() converts types, it should only affect the specific + style instance, not any styles it was coalesced from. + """ + # Create default style with string values (as they come from YAML) + default_style = Style( + text_data=TextData(color="white", size="60"), # String values from YAML + box_data=BoxData(box_width="30") + ) + + # Create subtitle style and coalesce + sub_style = Style() + sub_style.coalesce(default_style) + + # Call correct_values to convert types + sub_style.correct_values() + + # After correct_values, subtitle style should have converted types + assert sub_style.text_data.color == (255, 255, 255) + assert sub_style.text_data.size == 60 + assert isinstance(sub_style.text_data.size, int) + + # Original default style should remain unchanged + assert default_style.text_data.color == "white", \ + "Default style should still have original string value" + assert default_style.text_data.size == "60", \ + "Default style should still have original string value" + + +def test_character_dialogue_config_coalesce_independence(): + """ + CharacterDialogueConfig.coalesce() should create independent nested style objects. + """ + # Create two configs with nested styles + char_style = Style(text_data=TextData(color=(255, 215, 0), size=24)) + dialogue_style = Style(text_data=TextData(color=(255, 255, 255), size=20)) + + config1 = CharacterDialogueConfig( + enabled=True, + character_name="Romeo", + character=char_style, + dialogue=dialogue_style + ) + + config2 = CharacterDialogueConfig() + + # Coalesce config2 with config1 + config2.coalesce(config1) + + # Config2 should have the styles + assert config2.character is not None + assert config2.dialogue is not None + + # But they should be independent objects + assert config2.character is not config1.character, \ + "config2.character should be independent of config1.character" + assert config2.dialogue is not config1.dialogue, \ + "config2.dialogue should be independent of config1.dialogue" + + # Mutating config2 should not affect config1 + config2.character.text_data.color = (0, 255, 0) # GREEN + + assert config1.character.text_data.color == (255, 215, 0), \ + "Mutating config2 should not affect config1" + + +def test_multiple_subtitles_have_independent_styles(): + """ + Multiple subtitles extracted in the same operation should each have + independent style objects. + """ + with tempfile.TemporaryDirectory() as temp_dir: + # Create test image + image = Image.new('RGB', (800, 600), (255, 255, 255)) + image_path = os.path.join(temp_dir, 'test.png') + image.save(image_path) + image.close() + + # Create styles with string values (as from YAML) + styles_yaml = [ + { + 'style_id': 'default', + 'text_data': { + 'color': 'white', # String value + 'size': '60' # String value + } + } + ] + styles = extract_styles(styles_yaml) + + # Store original default style color for comparison + original_default_color = styles['default'].text_data.color + assert original_default_color == 'white' + + # Create draft with multiple subtitles + draft = """ +image_id: test.png +content: First subtitle +content: Second subtitle +content: Third subtitle +""" + + with tempfile.TemporaryDirectory() as output_dir: + subtitle_groups = extract_subtitle_groups( + draft_id='test_draft', + draft_body=draft, + styles=styles, + image_dir=temp_dir, + output_dir=output_dir + ) + + subtitles = subtitle_groups['test.png'][0].subtitles + assert len(subtitles) == 3 + + # Each subtitle should have independent text_data objects + assert subtitles[0].style.text_data is not subtitles[1].style.text_data, \ + "Subtitle 0 and 1 should have independent text_data" + assert subtitles[1].style.text_data is not subtitles[2].style.text_data, \ + "Subtitle 1 and 2 should have independent text_data" + assert subtitles[0].style.text_data is not styles['default'].text_data, \ + "Subtitle should not share text_data with default style" + + # Mutating one subtitle should not affect others + subtitles[0].style.text_data.color = (255, 0, 0) # RED + + assert subtitles[1].style.text_data.color == (255, 255, 255), \ + "Mutating subtitle 0 should not affect subtitle 1" + assert subtitles[2].style.text_data.color == (255, 255, 255), \ + "Mutating subtitle 0 should not affect subtitle 2" + + +def test_styles_dictionary_unchanged_across_extractions(): + """ + The styles dictionary should remain unchanged when used across multiple + subtitle extraction operations. + """ + with tempfile.TemporaryDirectory() as temp_dir: + # Create test image + image = Image.new('RGB', (800, 600), (255, 255, 255)) + image_path = os.path.join(temp_dir, 'test.png') + image.save(image_path) + image.close() + + # Create styles dictionary (loaded once and reused) + styles_yaml = [ + { + 'style_id': 'default', + 'text_data': { + 'color': 'white', + 'size': '60' + } + } + ] + styles = extract_styles(styles_yaml) + + # Store the original values + original_text_data = styles['default'].text_data + original_color = deepcopy(styles['default'].text_data.color) + + draft = """ +image_id: test.png +content: Test subtitle +""" + + with tempfile.TemporaryDirectory() as output_dir: + # First extraction + extract_subtitle_groups( + draft_id='draft1', + draft_body=draft, + styles=styles, + image_dir=temp_dir, + output_dir=output_dir + ) + + # Second extraction with same styles dictionary + extract_subtitle_groups( + draft_id='draft2', + draft_body=draft, + styles=styles, + image_dir=temp_dir, + output_dir=output_dir + ) + + # The styles dictionary should be unchanged + assert styles['default'].text_data.color == original_color, \ + "Styles dictionary should not be mutated by extraction" + + # The text_data object reference should be the same + assert styles['default'].text_data is original_text_data, \ + "Style dictionary should maintain same object references" + + +def test_nested_child_styles_are_independent(): + """ + When a parent style has child styles, correct_values() should ensure + all child styles have independent copies of inherited attributes. + """ + # Create parent style with sub-styles + parent = Style( + style_id='parent', + text_data=TextData(color='white', size='60'), + styles=[ + Style(style_id='child1', text_data=TextData(color='red')), + Style(style_id='child2', text_data=TextData(color='blue')) + ] + ) + + # Call correct_values - children should inherit but remain independent + parent.correct_values() + + # Child styles should have their own text_data objects + assert parent.styles[0].text_data is not parent.text_data, \ + "Child1 should not share text_data with parent" + assert parent.styles[1].text_data is not parent.text_data, \ + "Child2 should not share text_data with parent" + assert parent.styles[0].text_data is not parent.styles[1].text_data, \ + "Child1 and child2 should not share text_data" + + # Mutating child should not affect parent or siblings + parent.styles[0].text_data.size = 100 + + assert parent.text_data.size == 60, \ + "Mutating child should not affect parent" + assert parent.styles[1].text_data.size == 60, \ + "Mutating child1 should not affect child2" From 09cd5b8229186483a752eba9faa566475919be5b Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 17:46:34 -0800 Subject: [PATCH 10/13] another deep copy issue --- src/kksubs/data/subtitle/style_attributes.py | 14 +- src/kksubs/service/processor/apply_text.py | 5 + tests/test_kksubs/test_style_behavior.py | 261 +++++++++++++++++++ 3 files changed, 275 insertions(+), 5 deletions(-) create mode 100644 tests/test_kksubs/test_style_behavior.py diff --git a/src/kksubs/data/subtitle/style_attributes.py b/src/kksubs/data/subtitle/style_attributes.py index 3baee7b..e618cc9 100644 --- a/src/kksubs/data/subtitle/style_attributes.py +++ b/src/kksubs/data/subtitle/style_attributes.py @@ -45,9 +45,9 @@ def coalesce(self, other:"TextData"): return self.font = coalesce(self.font, other.font) self.size = coalesce(self.size, other.size) - self.color = coalesce(self.color, other.color) + self.color = deepcopy(self.color) if self.color is not None else deepcopy(other.color) self.stroke_size = coalesce(self.stroke_size, other.stroke_size) - self.stroke_color = coalesce(self.stroke_color, other.stroke_color) + self.stroke_color = deepcopy(self.stroke_color) if self.stroke_color is not None else deepcopy(other.stroke_color) self.alpha = coalesce(self.alpha, other.alpha) self.text = coalesce(self.text, other.text) @@ -154,9 +154,9 @@ def coalesce(self, other:"BoxData"): self.box_width = coalesce(self.box_width, other.box_width) if self.anchor is None and self.grid4 is None and self.grid10 is None: - self.anchor = other.anchor - self.grid4 = other.grid4 - self.grid10 = other.grid10 + self.anchor = deepcopy(other.anchor) if other.anchor is not None else None + self.grid4 = deepcopy(other.grid4) if other.grid4 is not None else None + self.grid10 = deepcopy(other.grid10) if other.grid10 is not None else None self.nudge = coalesce(self.nudge, other.nudge) self.rotate = coalesce(self.rotate, other.rotate) @@ -330,6 +330,8 @@ def get_default(cls): def deserialize(cls, path=None): if path is None: return None + if isinstance(path, str): + return Mask(path=path) return Mask(**path) def coalesce(self, other:"Mask"): @@ -361,6 +363,8 @@ def get_default(cls): def deserialize(cls, path=None): if path is None: return None + if isinstance(path, str): + return Background(path=path) return Background(**path) def coalesce(self, other:"Background"): diff --git a/src/kksubs/service/processor/apply_text.py b/src/kksubs/service/processor/apply_text.py index ec288a5..9591b9b 100644 --- a/src/kksubs/service/processor/apply_text.py +++ b/src/kksubs/service/processor/apply_text.py @@ -19,6 +19,11 @@ def create_text_layer( ) -> Image.Image: image_width, image_height = image.size + if isinstance(color, list): + color = tuple(color) + if isinstance(stroke_color, list): + stroke_color = tuple(stroke_color) + text_layer = Image.new("RGBA", image.size, (0, 0, 0, 0)) if content is None or not content or font is None: return text_layer diff --git a/tests/test_kksubs/test_style_behavior.py b/tests/test_kksubs/test_style_behavior.py new file mode 100644 index 0000000..4262d2e --- /dev/null +++ b/tests/test_kksubs/test_style_behavior.py @@ -0,0 +1,261 @@ +""" +Tests for style attribute behavior and functionality. + +These tests verify correct behavior for style data structures, +coalescing logic, and deserialization patterns. +""" +import tempfile +import os +from PIL import Image + +from kksubs.data.subtitle.style_attributes import ( + BoxData, Mask, Background, TextData, CharacterDialogueConfig +) +from kksubs.data.subtitle.style import Style +from kksubs.data.subtitle.subtitle import CharacterDialogueSubtitle +from kksubs.service.subtitle import _render_character_dialogue_subtitle +from kksubs.service.extraction.subtitle import extract_subtitle_groups +from kksubs.service.extraction.style import extract_styles + + +def test_boxdata_coalesce_independence(): + """ + BoxData.coalesce() should create independent copies of coordinate lists + to prevent shared references between different BoxData objects. + """ + box1 = BoxData() + box2 = BoxData(anchor=[100, 200], grid4=[2, 3], grid10=[5, 5]) + + box1.coalesce(box2) + + assert box1.anchor is not None + assert box1.grid4 is not None + assert box1.grid10 is not None + + box1.anchor[0] = 999 + assert box2.anchor[0] == 100, \ + "Modifying box1.anchor should not affect box2.anchor" + + box1.grid4[0] = 888 + assert box2.grid4[0] == 2, \ + "Modifying box1.grid4 should not affect box2.grid4" + + box1.grid10[1] = 777 + assert box2.grid10[1] == 5, \ + "Modifying box1.grid10 should not affect box2.grid10" + + +def test_textdata_coalesce_independence(): + """ + TextData.coalesce() should create independent copies of color lists + to prevent shared references between different TextData objects. + """ + text1 = TextData() + text2 = TextData(color=[255, 0, 0], stroke_color=[0, 0, 0]) + + text1.coalesce(text2) + + assert text1.color is not None + assert text1.stroke_color is not None + + text1.color[0] = 100 + assert text2.color[0] == 255, \ + "Modifying text1.color should not affect text2.color" + + text1.stroke_color[1] = 200 + assert text2.stroke_color[1] == 0, \ + "Modifying text1.stroke_color should not affect text2.stroke_color" + + +def test_mask_deserialize_string_path(): + """ + Mask.deserialize() should accept a string path in addition to dict format, + since YAML can represent paths as strings. + """ + mask = Mask.deserialize(path="mask.png") + + assert mask is not None + assert mask.path == "mask.png" + + +def test_mask_deserialize_dict(): + """ + Mask.deserialize() should accept dict format for backward compatibility. + """ + mask = Mask.deserialize(path={"path": "mask.png"}) + + assert mask is not None + assert mask.path == "mask.png" + + +def test_background_deserialize_string_path(): + """ + Background.deserialize() should accept a string path in addition to dict format, + since YAML can represent paths as strings. + """ + bg = Background.deserialize(path="bg.png") + + assert bg is not None + assert bg.path == "bg.png" + + +def test_background_deserialize_dict(): + """ + Background.deserialize() should accept dict format for backward compatibility. + """ + bg = Background.deserialize(path={"path": "bg.png"}) + + assert bg is not None + assert bg.path == "bg.png" + + +def test_character_dialogue_rendering_color_formats(): + """ + Character/dialogue rendering should handle color values as lists (from YAML) + and convert them to tuples for PIL compatibility. + """ + image = Image.new('RGBA', (800, 600), (255, 255, 255, 255)) + + char_style = Style.deserialize({ + 'text_data': { + 'color': [255, 0, 0], + 'size': 24, + } + }) + + dialogue_style = Style.deserialize({ + 'text_data': { + 'color': [255, 255, 255], + 'size': 20, + } + }) + + config = CharacterDialogueConfig( + enabled=True, + character_name="Test", + separator=": ", + spacing=0, + character=char_style, + dialogue=dialogue_style, + ) + + style = Style.deserialize({ + 'box_data': { + 'grid10': [5, 5], + 'align_h': 'center', + 'align_v': 'center', + } + }) + style.character_dialogue = config + + subtitle = CharacterDialogueSubtitle( + dialogue_content=["Hello world"], + style=style, + ) + + result = _render_character_dialogue_subtitle(image, subtitle, ".") + + assert result is not None + assert result.size == image.size + + +def test_character_dialogue_subtitle_independence(): + """ + Multiple character/dialogue subtitles should have independent styles + after extraction and coalescing. + """ + with tempfile.TemporaryDirectory() as temp_dir: + image = Image.new('RGB', (800, 600), (255, 255, 255)) + image_path = os.path.join(temp_dir, 'test.png') + image.save(image_path) + image.close() + + styles_yaml = [ + { + 'style_id': 'speaker', + 'character_dialogue': { + 'enabled': True, + 'character_name': 'Speaker', + 'character': { + 'text_data': {'color': [255, 215, 0], 'size': 24} + }, + 'dialogue': { + 'text_data': {'color': [255, 255, 255], 'size': 20} + } + } + } + ] + styles = extract_styles(styles_yaml) + + draft = """ +image_id: test.png +speaker: First line +speaker: Second line +speaker: Third line +""" + + with tempfile.TemporaryDirectory() as output_dir: + subtitle_groups = extract_subtitle_groups( + draft_id='test_draft', + draft_body=draft, + styles=styles, + image_dir=temp_dir, + output_dir=output_dir + ) + + subtitles = subtitle_groups['test.png'][0].subtitles + assert len(subtitles) == 3 + + for subtitle in subtitles: + assert isinstance(subtitle, CharacterDialogueSubtitle) + + assert subtitles[0].style.character_dialogue is not subtitles[1].style.character_dialogue, \ + "Subtitle 0 and 1 should have independent character_dialogue configs" + assert subtitles[1].style.character_dialogue is not subtitles[2].style.character_dialogue, \ + "Subtitle 1 and 2 should have independent character_dialogue configs" + + assert subtitles[0].style.character_dialogue.character is not \ + subtitles[1].style.character_dialogue.character, \ + "Character styles should be independent" + + subtitles[0].style.character_dialogue.character.text_data.color = (0, 255, 0) + + assert subtitles[1].style.character_dialogue.character.text_data.color != (0, 255, 0), \ + "Modifying subtitle 0 should not affect subtitle 1" + assert subtitles[2].style.character_dialogue.character.text_data.color != (0, 255, 0), \ + "Modifying subtitle 0 should not affect subtitle 2" + + +def test_character_dialogue_config_coalesce_independence(): + """ + CharacterDialogueConfig.coalesce() should create independent copies + of nested character and dialogue styles. + """ + char_style1 = Style(text_data=TextData(color=[255, 0, 0], size=24)) + dialogue_style1 = Style(text_data=TextData(color=[255, 255, 255], size=20)) + + config1 = CharacterDialogueConfig( + enabled=True, + character_name="Speaker1", + character=char_style1, + dialogue=dialogue_style1 + ) + + config2 = CharacterDialogueConfig() + config2.coalesce(config1) + + assert config2.character is not None + assert config2.dialogue is not None + + assert config2.character is not config1.character, \ + "Coalesced character style should be independent" + assert config2.dialogue is not config1.dialogue, \ + "Coalesced dialogue style should be independent" + + assert config2.character.text_data is not config1.character.text_data, \ + "Coalesced character text_data should be independent" + + config2.character.text_data.color[0] = 100 + + assert config1.character.text_data.color[0] == 255, \ + "Modifying config2 should not affect config1" \ No newline at end of file From 7813ee4e0a6a508da4d7febc7be0d412e4be782d Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:01:57 -0800 Subject: [PATCH 11/13] chara dialogue bug --- src/kksubs/service/subtitle.py | 9 +-- .../test_character_dialogue_integration.py | 56 +++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/kksubs/service/subtitle.py b/src/kksubs/service/subtitle.py index ef0d573..d483251 100644 --- a/src/kksubs/service/subtitle.py +++ b/src/kksubs/service/subtitle.py @@ -145,12 +145,9 @@ def _render_character_dialogue_subtitle( dialogue_style = config.dialogue if config.dialogue is not None else style # Ensure styles have defaults coalesced - if char_style.box_data.box_width is None: - from kksubs.data.subtitle.style_attributes import BoxData - char_style.box_data.coalesce(BoxData.get_default()) - if dialogue_style.box_data.box_width is None: - from kksubs.data.subtitle.style_attributes import BoxData - dialogue_style.box_data.coalesce(BoxData.get_default()) + from kksubs.data.subtitle.style import Style + char_style.coalesce(Style.get_default()) + dialogue_style.coalesce(Style.get_default()) # Get character font char_text_data = char_style.text_data diff --git a/tests/test_kksubs/test_character_dialogue_integration.py b/tests/test_kksubs/test_character_dialogue_integration.py index 31e52d3..f1516f8 100644 --- a/tests/test_kksubs/test_character_dialogue_integration.py +++ b/tests/test_kksubs/test_character_dialogue_integration.py @@ -154,3 +154,59 @@ def test_mixed_subtitle_types_workflow(): output_image = Image.open(output_image_path) assert output_image.size == (1920, 1080) output_image.close() + + +def test_missing_font_size_rendering(): + """Test that missing font size does not crash rendering.""" + with tempfile.TemporaryDirectory() as test_dir: + controller = SubtitleController(test_dir) + controller.create() + + image_dir = controller.get_image_directory() + generate_test_image(image_dir, 'bug_repro.png') + + styles_content = """ +- style_id: buggy_style + character_dialogue: + enabled: true + character_name: "Buggy" + character: + text_data: + color: [255, 0, 0] + dialogue: + text_data: + color: [0, 255, 0] +""" + styles_path = os.path.join(controller.workspace_directory, 'styles.yml') + with open(styles_path, 'w') as f: + f.write(styles_content) + + scripts_dir = controller.get_scripts_directory() + draft_files = os.listdir(scripts_dir) + draft_path = os.path.join(scripts_dir, draft_files[0]) + + draft_content = """ +image_id: bug_repro.png +buggy_style: Text content +""" + with open(draft_path, 'w') as f: + f.write(draft_content) + + # Should not crash and SHOULD render something (not return original image) + controller.add_subtitles(allow_multiprocessing=False) + + output_dir = controller.get_output_directory_by_script(draft_files[0]) + output_image_path = os.path.join(output_dir, 'bug_repro.png') + assert os.path.exists(output_image_path) + + # Verify that the image was actually modified (subtitles rendered) + # If the bug exists, it returns the original image, so this should fail. + # We need to compare pixel data or similar. + output_image = Image.open(output_image_path) + input_image = Image.open(os.path.join(image_dir, 'bug_repro.png')) + + # Simple check: different content + assert list(output_image.getdata()) != list(input_image.getdata()), "Output image is identical to input, implying render failure." + + output_image.close() + input_image.close() From 9955aa0c62ed1c58193888c83b223462dd2e6aee Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:10:33 -0800 Subject: [PATCH 12/13] move imports to top --- src/common/utils/file.py | 1 - src/kkp/cmd/project.py | 2 +- src/kkp/controller/project.py | 7 ++----- src/kksubs/cmd/subtitle.py | 2 +- src/kksubs/controller/subtitle.py | 2 +- src/kksubs/data/subtitle/style_attributes.py | 7 +++---- src/kksubs/service/subtitle.py | 2 +- 7 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/common/utils/file.py b/src/common/utils/file.py index abcd45b..8e0d5b2 100644 --- a/src/common/utils/file.py +++ b/src/common/utils/file.py @@ -27,7 +27,6 @@ def open_directory(path): FileNotFoundError: If the directory doesn't exist OSError: If the directory cannot be opened """ - from pathlib import Path # Convert to Path object for consistency path = Path(path) diff --git a/src/kkp/cmd/project.py b/src/kkp/cmd/project.py index ad6c202..0e794ff 100644 --- a/src/kkp/cmd/project.py +++ b/src/kkp/cmd/project.py @@ -1,5 +1,6 @@ import argparse import logging +from common.import_utils import get_kksubs_version from kkp.controller.project import ProjectController @@ -79,7 +80,6 @@ def command_line(): get_version = args.version if get_version: - from common.import_utils import get_kksubs_version print(f'kksubs version {get_kksubs_version()}') return diff --git a/src/kkp/controller/project.py b/src/kkp/controller/project.py index 259dc38..2eae31f 100644 --- a/src/kkp/controller/project.py +++ b/src/kkp/controller/project.py @@ -1,6 +1,7 @@ import logging import os import shutil +import platform from pathlib import Path from typing import Dict, List, Optional import yaml @@ -8,6 +9,7 @@ import subprocess from packaging import version +from common.utils.file import open_directory from common.import_utils import get_kksubs_version from kkp.data.config import KKPSettings from kksubs.service.file import FileService @@ -506,7 +508,6 @@ def _clean_userdata_directory(self): userdata_path = os.path.join(self.game_directory, 'UserData') if os.path.exists(userdata_path): - import shutil logger.info(f"Cleaning up UserData directory: {userdata_path}") shutil.rmtree(userdata_path) logger.info("UserData directory cleaned up successfully") @@ -861,7 +862,6 @@ def save_current_project(self): def _find_executable(self, base_name: str) -> Optional[str]: """Find executable with cross-platform support.""" - import platform if self.game_directory is None: return None @@ -909,7 +909,6 @@ def open_library_directory(self): raise ValueError("Library is None") if not self.library.exists(): raise FileNotFoundError(self.library) - from common.utils.file import open_directory open_directory(self.library) def open_output_folders(self, drafts:Optional[str]=None): @@ -917,7 +916,6 @@ def open_output_folders(self, drafts:Optional[str]=None): Open the subtitled output images in file explorer. """ output_dir = self.get_output_directory() - from common.utils.file import open_directory if drafts is not None and not drafts: for draft in drafts: @@ -975,7 +973,6 @@ def export_gallery( print(f'Finished exporting {len(projects)} projects.') if show_destination: - from common.utils.file import open_directory open_directory(destination) def _merge_project(self, project_name:str): diff --git a/src/kksubs/cmd/subtitle.py b/src/kksubs/cmd/subtitle.py index c11935b..5312321 100644 --- a/src/kksubs/cmd/subtitle.py +++ b/src/kksubs/cmd/subtitle.py @@ -1,5 +1,6 @@ import argparse import logging +from common.import_utils import get_kksubs_version from kksubs.controller.subtitle import SubtitleController @@ -61,7 +62,6 @@ def command_line(): get_version:bool = args.version if get_version: - from common.import_utils import get_kksubs_version print(f'kksubs version {get_kksubs_version()}') return diff --git a/src/kksubs/controller/subtitle.py b/src/kksubs/controller/subtitle.py index 5ed6457..1ced9d1 100644 --- a/src/kksubs/controller/subtitle.py +++ b/src/kksubs/controller/subtitle.py @@ -2,6 +2,7 @@ from kksubs.service.sub_project import SubtitleProjectService from typing import Dict, List import logging +from common.utils.file import open_directory from kksubs.watcher.subtitle import SubtitleWatcher @@ -71,7 +72,6 @@ def add_subtitles( def open_output_folders(self, drafts:str=None): # open the folder containing subtitled images corresponding to given draft. # if draft is not given, opens every folder in the outputs folder. - from common.utils.file import open_directory output_dir = self.get_output_directory() if drafts is not None and not drafts: diff --git a/src/kksubs/data/subtitle/style_attributes.py b/src/kksubs/data/subtitle/style_attributes.py index e618cc9..579833a 100644 --- a/src/kksubs/data/subtitle/style_attributes.py +++ b/src/kksubs/data/subtitle/style_attributes.py @@ -1,5 +1,6 @@ from copy import deepcopy +import kksubs.data.subtitle.style from kksubs.data.abstract import BaseData from common.utils.coalesce import coalesce from kksubs.utils.sanitizers import to_float, to_integer, to_rgb_color, to_string, to_validated_value, to_xy_coords @@ -413,8 +414,6 @@ def deserialize(cls, data=None): if data is None: return None - from kksubs.data.subtitle.style import Style - enabled = data.get('enabled', False) character_name = data.get('character_name', "") separator = data.get('separator', ": ") @@ -423,8 +422,8 @@ def deserialize(cls, data=None): character_data = data.get('character') dialogue_data = data.get('dialogue') - character = Style.deserialize(character_data) if character_data is not None else None - dialogue = Style.deserialize(dialogue_data) if dialogue_data is not None else None + character = kksubs.data.subtitle.style.Style.deserialize(character_data) if character_data is not None else None + dialogue = kksubs.data.subtitle.style.Style.deserialize(dialogue_data) if dialogue_data is not None else None return CharacterDialogueConfig( enabled=enabled, diff --git a/src/kksubs/service/subtitle.py b/src/kksubs/service/subtitle.py index d483251..e9ec220 100644 --- a/src/kksubs/service/subtitle.py +++ b/src/kksubs/service/subtitle.py @@ -6,6 +6,7 @@ from common.utils.coalesce import coalesce from kksubs.data.subtitle.style_attributes import OutlineData from kksubs.data.subtitle.subtitle import CharacterDialogueSubtitle, Subtitle +from kksubs.data.subtitle.style import Style # from kksubs.data.subtitle.subtitle import OutlineData, Subtitle from kksubs.service.processor.motion_blur import apply_motion_blur from kksubs.service.processor.apply_text import create_text_layer @@ -145,7 +146,6 @@ def _render_character_dialogue_subtitle( dialogue_style = config.dialogue if config.dialogue is not None else style # Ensure styles have defaults coalesced - from kksubs.data.subtitle.style import Style char_style.coalesce(Style.get_default()) dialogue_style.coalesce(Style.get_default()) From 67fd7a1365859f3d04b360285aef76f930e42f29 Mon Sep 17 00:00:00 2001 From: psilabs-dev <113860476+psilabs-dev@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:14:36 -0800 Subject: [PATCH 13/13] reduce testing noise --- tests/test_kksubs/test_character_dialogue_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_kksubs/test_character_dialogue_integration.py b/tests/test_kksubs/test_character_dialogue_integration.py index f1516f8..370004c 100644 --- a/tests/test_kksubs/test_character_dialogue_integration.py +++ b/tests/test_kksubs/test_character_dialogue_integration.py @@ -206,7 +206,7 @@ def test_missing_font_size_rendering(): input_image = Image.open(os.path.join(image_dir, 'bug_repro.png')) # Simple check: different content - assert list(output_image.getdata()) != list(input_image.getdata()), "Output image is identical to input, implying render failure." + assert output_image.tobytes() != input_image.tobytes(), "Output image is identical to input, implying render failure." output_image.close() input_image.close()