opensportslib 0.3.1__tar.gz → 0.3.1.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. {opensportslib-0.3.1/opensportslib.egg-info → opensportslib-0.3.1.dev2}/PKG-INFO +14 -4
  2. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/README.md +13 -3
  3. opensportslib-0.3.1.dev2/opensportslib/apis/base_task_model.py +550 -0
  4. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/apis/classification.py +32 -0
  5. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/apis/localization.py +32 -0
  6. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/apis/vqa.py +62 -0
  7. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/vqa/qwen3_vl_native.yaml +6 -0
  8. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/datasets/vqa_dataset.py +16 -11
  9. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/tools/__init__.py +23 -0
  10. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/tools/hf_transfer.py +899 -65
  11. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/tools/parquet_to_osl_json.py +64 -5
  12. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2/opensportslib.egg-info}/PKG-INFO +14 -4
  13. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/pyproject.toml +1 -1
  14. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_hf_transfer_tools.py +824 -11
  15. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_task_model_api_contract.py +12 -3
  16. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/download/download_osl_hf.py +8 -0
  17. opensportslib-0.3.1/opensportslib/apis/base_task_model.py +0 -154
  18. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/LICENSE +0 -0
  19. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/LICENSE-COMMERCIAL +0 -0
  20. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/MANIFEST.in +0 -0
  21. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/examples/quickstart/basic_classification.py +0 -0
  22. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/examples/quickstart/basic_localization.py +0 -0
  23. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/examples/quickstart/basic_vqa.py +0 -0
  24. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/__init__.py +0 -0
  25. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/adaptation/__init__.py +0 -0
  26. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/adaptation/spotta.py +0 -0
  27. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/apis/__init__.py +0 -0
  28. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/cli.py +0 -0
  29. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/classification/default.yaml +0 -0
  30. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/classification/sngar_frames.yaml +0 -0
  31. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/classification/sngar_tracking.yaml +0 -0
  32. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/classification/video.yaml +0 -0
  33. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/default.yaml +0 -0
  34. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/localization/calf_resnetpca512.yaml +0 -0
  35. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/localization/default.yaml +0 -0
  36. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/localization/e2e_spotta.yaml +0 -0
  37. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/localization/h5_header_distance.yaml +0 -0
  38. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/localization/h5_header_skeleton.yaml +0 -0
  39. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/localization/netvladpp_resnetpca512.yaml +0 -0
  40. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/localization/tracking_action_spotting.yaml +0 -0
  41. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/localization/video_dali.yaml +0 -0
  42. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/localization/video_ocv.yaml +0 -0
  43. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/vqa/default.yaml +0 -0
  44. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/vqa/qwen.yaml +0 -0
  45. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/vqa/qwen_lora.yaml +0 -0
  46. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/vqa/qwen_sngar_frames.yaml +0 -0
  47. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/configs/vqa/xvars.yaml +0 -0
  48. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/__init__.py +0 -0
  49. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/__init__.py +0 -0
  50. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/accessors.py +0 -0
  51. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/conflicts.py +0 -0
  52. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/loader.py +0 -0
  53. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/migrate.py +0 -0
  54. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/migrations/__init__.py +0 -0
  55. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/migrations/legacy_to_canonical.py +0 -0
  56. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/runtime_adapter.py +0 -0
  57. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/schema.py +0 -0
  58. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/schemas/__init__.py +0 -0
  59. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/schemas/schema_canonical.py +0 -0
  60. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/schemas/schema_legacy.py +0 -0
  61. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/config/validate.py +0 -0
  62. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/loss/__init__.py +0 -0
  63. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/loss/builder.py +0 -0
  64. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/loss/calf.py +0 -0
  65. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/loss/ce.py +0 -0
  66. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/loss/combine.py +0 -0
  67. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/loss/nll.py +0 -0
  68. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/optimizer/__init__.py +0 -0
  69. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/optimizer/builder.py +0 -0
  70. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/sampler/weighted_sampler.py +0 -0
  71. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/scheduler/__init__.py +0 -0
  72. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/scheduler/builder.py +0 -0
  73. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/trainer/__init__.py +0 -0
  74. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/trainer/classification_trainer.py +0 -0
  75. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/trainer/localization_trainer.py +0 -0
  76. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/trainer/vqa_trainer.py +0 -0
  77. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/utils/checkpoint.py +0 -0
  78. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/utils/config.py +0 -0
  79. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/utils/config_normalize.py +0 -0
  80. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/utils/data.py +0 -0
  81. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/utils/ddp.py +0 -0
  82. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/utils/default_args.py +0 -0
  83. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/utils/hf_runtime.py +0 -0
  84. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/utils/lightning.py +0 -0
  85. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/utils/load_annotations.py +0 -0
  86. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/utils/seed.py +0 -0
  87. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/utils/video_processing.py +0 -0
  88. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/core/utils/wandb.py +0 -0
  89. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/datasets/__init__.py +0 -0
  90. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/datasets/builder.py +0 -0
  91. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/datasets/classification_dataset.py +0 -0
  92. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/datasets/localization_dataset.py +0 -0
  93. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/datasets/utils/__init__.py +0 -0
  94. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/datasets/utils/h5_tracking.py +0 -0
  95. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/datasets/utils/tracking.py +0 -0
  96. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/legacy_config/classification.yaml +0 -0
  97. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/legacy_config/localization-e2e-ocv.yaml +0 -0
  98. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/legacy_config/localization-json_calf_resnetpca512.yaml +0 -0
  99. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/legacy_config/localization-json_netvlad++_resnetpca512.yaml +0 -0
  100. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/legacy_config/localization.yaml +0 -0
  101. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/legacy_config/sngar-frames.yaml +0 -0
  102. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/legacy_config/sngar-tracking.yaml +0 -0
  103. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/metrics/classification_metric.py +0 -0
  104. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/metrics/localization_metric.py +0 -0
  105. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/metrics/vqa_metric.py +0 -0
  106. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/__init__.py +0 -0
  107. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/backbones/builder.py +0 -0
  108. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/base/contextaware.py +0 -0
  109. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/base/e2e.py +0 -0
  110. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/base/learnablepooling.py +0 -0
  111. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/base/qwen_vl_native.py +0 -0
  112. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/base/qwen_xvars.py +0 -0
  113. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/base/rule_based.py +0 -0
  114. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/base/tracking.py +0 -0
  115. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/base/vars.py +0 -0
  116. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/base/video.py +0 -0
  117. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/base/video_chatgpt_compat.py +0 -0
  118. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/base/video_mae.py +0 -0
  119. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/base/xvars_videochatgpt.py +0 -0
  120. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/builder.py +0 -0
  121. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/heads/builder.py +0 -0
  122. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/neck/builder.py +0 -0
  123. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/common.py +0 -0
  124. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/impl/__init__.py +0 -0
  125. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/impl/asformer.py +0 -0
  126. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/impl/calf.py +0 -0
  127. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/impl/gsm.py +0 -0
  128. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/impl/gtad.py +0 -0
  129. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/impl/tsm.py +0 -0
  130. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/litebase.py +0 -0
  131. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/modules.py +0 -0
  132. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/shift.py +0 -0
  133. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/utils.py +0 -0
  134. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/vqa_prediction_priors.py +0 -0
  135. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/vqa_prompting.py +0 -0
  136. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/models/utils/xvars_clip_index.py +0 -0
  137. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/setup/setup.py +0 -0
  138. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/tools/_common.py +0 -0
  139. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib/tools/osl_json_to_parquet.py +0 -0
  140. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib.egg-info/SOURCES.txt +0 -0
  141. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib.egg-info/dependency_links.txt +0 -0
  142. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib.egg-info/entry_points.txt +0 -0
  143. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib.egg-info/requires.txt +0 -0
  144. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/opensportslib.egg-info/top_level.txt +0 -0
  145. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/scripts/run_h5_header_rule_inference.py +0 -0
  146. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/setup.cfg +0 -0
  147. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/conftest.py +0 -0
  148. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/release/__init__.py +0 -0
  149. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/release/_release_common.py +0 -0
  150. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/release/test_classification_release.py +0 -0
  151. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/release/test_localization_release.py +0 -0
  152. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/release/test_vqa_release.py +0 -0
  153. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_classification_dataset_paths.py +0 -0
  154. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_classification_trainer_dataloader.py +0 -0
  155. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_config_architecture.py +0 -0
  156. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_config_split_override_sync.py +0 -0
  157. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_config_utils_smoke.py +0 -0
  158. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_conversion_tools.py +0 -0
  159. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_extract_xvars_features.py +0 -0
  160. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_h5_header_rule_spotter.py +0 -0
  161. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_h5_header_skeleton_spotter.py +0 -0
  162. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_h5_tracking_dataset.py +0 -0
  163. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_localization_dali_filenames.py +0 -0
  164. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_localization_hf_backend_override.py +0 -0
  165. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_localization_intervals.py +0 -0
  166. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_package_smoke.py +0 -0
  167. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_pretrained_config_merge_policy.py +0 -0
  168. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_public_apis_smoke.py +0 -0
  169. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_setup_cli.py +0 -0
  170. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_spotta_e2e.py +0 -0
  171. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_subset_train_infer_integration.py +0 -0
  172. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_vqa_api.py +0 -0
  173. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_vqa_metrics_semantic.py +0 -0
  174. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_vqa_qwen_xvars.py +0 -0
  175. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_vqa_training_lora.py +0 -0
  176. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tests/test_vqa_xvars_videochatgpt.py +0 -0
  177. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/convert/build_sn_vqa_2026_vqa.py +0 -0
  178. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/convert/build_sngar_spotting.py +0 -0
  179. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/convert/build_soccernet_gar.py +0 -0
  180. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/convert/build_soccernet_gar_action_spotting.py +0 -0
  181. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/convert/build_soccernet_gar_vqa.py +0 -0
  182. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/convert/build_xvars_indexes.py +0 -0
  183. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/convert/extract_xvars_clip_features.py +0 -0
  184. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/convert/osl_json_to_parquet_webdataset.py +0 -0
  185. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/convert/parquet_webdataset_to_osl_json.py +0 -0
  186. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/convert/sngar_dataset_card.py +0 -0
  187. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/convert/sngar_events.py +0 -0
  188. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/convert/verify_sngar_spotting.py +0 -0
  189. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/download/download_hf_repo.py +0 -0
  190. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/download/push_sngar_spotting.py +0 -0
  191. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/download/upload_osl_hf.py +0 -0
  192. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/training/classification.py +0 -0
  193. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/training/localization.py +0 -0
  194. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/training/vqa.py +0 -0
  195. {opensportslib-0.3.1 → opensportslib-0.3.1.dev2}/tools/upload/upload_model_hf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opensportslib
3
- Version: 0.3.1
3
+ Version: 0.3.1.dev2
4
4
  Summary: OpenSportsLib is the professional library, designed for advanced video understanding in sports. It provides state-of-the-art tools for action recognition, spotting, retrieval, and captioning, making it ideal for researchers, analysts, and developers working with sports video data.
5
5
  Author: Jeet Vora
6
6
  Requires-Python: >=3.12
@@ -372,6 +372,7 @@ OpenSportsLib provides APIs and scripts for downloading and uploading OSL datase
372
372
  ```python
373
373
  from opensportslib.tools import (
374
374
  download_dataset_split_from_hf,
375
+ download_dataset_sample_inputs_from_hf,
375
376
  upload_dataset_inputs_from_json_to_hf,
376
377
  upload_dataset_as_parquet_to_hf,
377
378
  )
@@ -380,13 +381,22 @@ from opensportslib.tools import (
380
381
  ### Scripts
381
382
 
382
383
  ```bash
383
- python tools/download/download_osl_hf.py --repo-id <org/repo> --revision main --split test --format parquet --output-dir downloaded_data
384
+ python tools/download/download_osl_hf.py --repo-id <org/repo> --revision main --split test --format parquet --output-dir downloaded_data --annotations-only
384
385
  python tools/download/upload_osl_hf.py --repo-id <org/repo> --json-path <local_dataset.json> --split test --revision main
385
386
  ```
386
387
 
387
388
  Downloads are placed under `<output-dir>/<revision>/<split>`.
388
- For Parquet/WebDataset downloads, an existing `<split>.json` in that directory
389
- is reused without downloading or converting the split again.
389
+ Pass `annotations_only=True` to download or reconstruct only `<split>.json`.
390
+ The JSON records the resolved Hugging Face commit and can later be passed to
391
+ `download_dataset_sample_inputs_from_hf()` to fetch one sample or input. A full
392
+ Parquet/WebDataset download always completes the local split even when a
393
+ metadata-only `<split>.json` already exists.
394
+
395
+ JSON uploads support partially downloaded datasets: the JSON and all
396
+ referenced files available locally are committed, while missing referenced
397
+ files are skipped and reported. Remote files not included in that commit are
398
+ left untouched. Parquet/WebDataset uploads remain strict and require every
399
+ referenced file locally before conversion.
390
400
 
391
401
  ---
392
402
 
@@ -336,6 +336,7 @@ OpenSportsLib provides APIs and scripts for downloading and uploading OSL datase
336
336
  ```python
337
337
  from opensportslib.tools import (
338
338
  download_dataset_split_from_hf,
339
+ download_dataset_sample_inputs_from_hf,
339
340
  upload_dataset_inputs_from_json_to_hf,
340
341
  upload_dataset_as_parquet_to_hf,
341
342
  )
@@ -344,13 +345,22 @@ from opensportslib.tools import (
344
345
  ### Scripts
345
346
 
346
347
  ```bash
347
- python tools/download/download_osl_hf.py --repo-id <org/repo> --revision main --split test --format parquet --output-dir downloaded_data
348
+ python tools/download/download_osl_hf.py --repo-id <org/repo> --revision main --split test --format parquet --output-dir downloaded_data --annotations-only
348
349
  python tools/download/upload_osl_hf.py --repo-id <org/repo> --json-path <local_dataset.json> --split test --revision main
349
350
  ```
350
351
 
351
352
  Downloads are placed under `<output-dir>/<revision>/<split>`.
352
- For Parquet/WebDataset downloads, an existing `<split>.json` in that directory
353
- is reused without downloading or converting the split again.
353
+ Pass `annotations_only=True` to download or reconstruct only `<split>.json`.
354
+ The JSON records the resolved Hugging Face commit and can later be passed to
355
+ `download_dataset_sample_inputs_from_hf()` to fetch one sample or input. A full
356
+ Parquet/WebDataset download always completes the local split even when a
357
+ metadata-only `<split>.json` already exists.
358
+
359
+ JSON uploads support partially downloaded datasets: the JSON and all
360
+ referenced files available locally are committed, while missing referenced
361
+ files are skipped and reported. Remote files not included in that commit are
362
+ left untouched. Parquet/WebDataset uploads remain strict and require every
363
+ referenced file locally before conversion.
354
364
 
355
365
  ---
356
366
 
@@ -0,0 +1,550 @@
1
+ """Shared task-level wrapper base for OpenSportsLib APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import copy
9
+ import tempfile
10
+ import time
11
+ import uuid
12
+ import zipfile
13
+ from abc import ABC, abstractmethod
14
+ from pathlib import Path
15
+ from typing import Any
16
+ from urllib import error as urlerror
17
+ from urllib import request as urlrequest
18
+
19
+ from opensportslib.core.config.accessors import get_component_name_by_kind
20
+ from opensportslib.core.utils.config import (
21
+ expand,
22
+ load_config_omega,
23
+ fetch_and_merge_config_from_HF,
24
+ resolve_config_path,
25
+ resolve_inference_class_metadata,
26
+ )
27
+
28
+
29
+ def _manifest_media_references(payload: dict[str, Any]):
30
+ """Yield mutable manifest media references as (value, setter) pairs."""
31
+
32
+ for sample in payload.get("data", []):
33
+ if not isinstance(sample, dict):
34
+ continue
35
+ if isinstance(sample.get("video_path"), str):
36
+ yield sample["video_path"], lambda value, sample=sample: sample.__setitem__("video_path", value)
37
+ for key in ("frame_paths", "video_frames"):
38
+ values = sample.get(key)
39
+ if isinstance(values, list):
40
+ for index, value in enumerate(values):
41
+ if isinstance(value, str):
42
+ yield value, lambda replacement, values=values, index=index: values.__setitem__(index, replacement)
43
+ inputs = sample.get("inputs")
44
+ if not isinstance(inputs, list):
45
+ continue
46
+ for input_obj in inputs:
47
+ if not isinstance(input_obj, dict):
48
+ continue
49
+ for key in ("path", "ball_path"):
50
+ if isinstance(input_obj.get(key), str):
51
+ yield input_obj[key], lambda value, input_obj=input_obj, key=key: input_obj.__setitem__(key, value)
52
+ values = input_obj.get("paths")
53
+ if isinstance(values, list):
54
+ for index, value in enumerate(values):
55
+ if isinstance(value, str):
56
+ yield value, lambda replacement, values=values, index=index: values.__setitem__(index, replacement)
57
+
58
+
59
+ class BaseTaskModel(ABC):
60
+ """Thin shared contract for task-level OpenSportsLib wrappers."""
61
+
62
+ def __init__(
63
+ self,
64
+ config=None,
65
+ weights=None,
66
+ remote: str | None = None,
67
+ remote_timeout: float = 30.0,
68
+ remote_poll_interval: float = 1.0,
69
+ remote_result_timeout: float = 7200.0,
70
+ remote_model_id: str | None = None,
71
+ ):
72
+ self._configure_logging()
73
+ self.remote = remote.rstrip("/") if remote else None
74
+ self.remote_timeout = float(remote_timeout)
75
+ self.remote_poll_interval = float(remote_poll_interval)
76
+ self.remote_result_timeout = float(remote_result_timeout)
77
+ self.remote_model_id = remote_model_id
78
+ if self.remote_timeout <= 0 or self.remote_poll_interval <= 0 or self.remote_result_timeout <= 0:
79
+ raise ValueError("Remote timeout values must be positive.")
80
+
81
+ if config is None:
82
+ raise ValueError("config path is required")
83
+
84
+ self.config_path = resolve_config_path(config)
85
+ self.config = load_config_omega(self.config_path)
86
+ self.last_loaded_weights = None
87
+ self.best_checkpoint = None
88
+
89
+ if weights is not None and not self.is_remote:
90
+ self.config = fetch_and_merge_config_from_HF(self.config, weights, merge_policy="compatibility")
91
+ self.config = resolve_inference_class_metadata(self.config)
92
+ self.last_loaded_weights = weights
93
+ self.best_checkpoint = weights
94
+ elif weights is not None:
95
+ # The remote worker owns model loading; retain this only as caller metadata.
96
+ self.last_loaded_weights = weights
97
+ self.best_checkpoint = weights
98
+
99
+ self.train_flag = False # Flag to indicate whether we're in training mode (affects checkpoint loading behavior)
100
+
101
+ data_cfg = getattr(self.config, "DATA", None)
102
+ if data_cfg is not None and hasattr(data_cfg, "data_dir"):
103
+ data_cfg.data_dir = expand(data_cfg.data_dir)
104
+ logging.info(f"Data directory: {data_cfg.data_dir}")
105
+
106
+ self.run_id = os.environ.get("RUN_ID") or str(uuid.uuid4())[:8]
107
+ os.environ["RUN_ID"] = self.run_id
108
+
109
+ system_cfg = getattr(self.config, "SYSTEM", None)
110
+ if system_cfg is not None:
111
+ system_paths = getattr(system_cfg, "paths", None)
112
+ base_save_dir = expand(
113
+ getattr(system_paths, "save_dir", None)
114
+ or getattr(system_cfg, "save_dir", None)
115
+ or "./checkpoints"
116
+ )
117
+ model_name = get_component_name_by_kind(self.config, "encoder") or "model"
118
+ run_save_dir = os.path.join(base_save_dir, model_name, self.run_id)
119
+ self.save_dir = run_save_dir
120
+ if system_paths is not None:
121
+ system_paths.save_dir = run_save_dir
122
+ if hasattr(system_paths, "work_dir"):
123
+ system_paths.work_dir = run_save_dir
124
+ else:
125
+ system_cfg.save_dir = run_save_dir
126
+ if hasattr(system_cfg, "work_dir"):
127
+ system_cfg.work_dir = run_save_dir
128
+ os.makedirs(run_save_dir, exist_ok=True)
129
+ else:
130
+ self.save_dir = expand("./checkpoints")
131
+ os.makedirs(self.save_dir, exist_ok=True)
132
+
133
+ logging.info(f"Save directory: {self.save_dir}")
134
+
135
+ self.model = None
136
+ self.processor = None
137
+ self.trainer = None
138
+ self.last_remote_failures: list[dict[str, Any]] = []
139
+
140
+ if weights is not None and not self.is_remote:
141
+ self.load_weights(weights=weights)
142
+
143
+ @property
144
+ def is_remote(self) -> bool:
145
+ """Whether inference requests are sent to an OpenSportsLib server."""
146
+
147
+ return self.remote is not None
148
+
149
+ def submit_inference(
150
+ self,
151
+ *,
152
+ task_type: str,
153
+ test_set: str,
154
+ model_id: str | None = None,
155
+ task_options: dict[str, Any] | None = None,
156
+ ) -> dict[str, Any]:
157
+ """Upload a complete JSON manifest and its referenced local media as one job."""
158
+
159
+ if not self.remote:
160
+ raise RuntimeError("Remote inference is not configured. Pass `remote=` to the model constructor.")
161
+ manifest_path = Path(expand(test_set)).resolve()
162
+ if not manifest_path.is_file():
163
+ raise FileNotFoundError(f"Test manifest not found: {manifest_path}")
164
+ if manifest_path.suffix.lower() != ".json":
165
+ raise ValueError("Remote test-set uploads currently require a JSON manifest.")
166
+
167
+ with tempfile.TemporaryDirectory(prefix="opensportslib-remote-") as temp_dir:
168
+ staged_manifest, archive_path = self._stage_manifest_upload(manifest_path, Path(temp_dir))
169
+ logging.info(
170
+ "Submitting remote test-set inference | task=%s manifest=%s manifest_bytes=%d archive=%s archive_bytes=%d",
171
+ task_type,
172
+ manifest_path,
173
+ staged_manifest.stat().st_size,
174
+ archive_path,
175
+ archive_path.stat().st_size,
176
+ )
177
+ return self._post_multipart(
178
+ "/predict",
179
+ fields={
180
+ "task_type": task_type,
181
+ "model_id": model_id or self.remote_model_id or "",
182
+ "task_options": json.dumps(task_options or {}),
183
+ },
184
+ files={
185
+ "test_set_file": staged_manifest,
186
+ "media_archive": archive_path,
187
+ },
188
+ )
189
+
190
+ def submit_per_sample_inference(
191
+ self,
192
+ *,
193
+ task_type: str,
194
+ test_set: str,
195
+ model_id: str | None = None,
196
+ task_options: dict[str, Any] | None = None,
197
+ ) -> dict[str, Any]:
198
+ """Submit one asynchronous remote job for every sample in an OSL manifest."""
199
+
200
+ if not self.remote:
201
+ raise RuntimeError("Remote inference is not configured. Pass `remote=` to the model constructor.")
202
+ manifest_path, payload = self._load_remote_manifest(test_set)
203
+ template = copy.deepcopy(payload)
204
+ samples = template.pop("data")
205
+ jobs: list[dict[str, Any]] = []
206
+ submission_errors: list[dict[str, Any]] = []
207
+
208
+ for sample_index, sample in enumerate(samples):
209
+ sample_id = str(sample.get("id") or sample_index) if isinstance(sample, dict) else str(sample_index)
210
+ record: dict[str, Any] = {"sample_index": sample_index, "sample_id": sample_id}
211
+ try:
212
+ with tempfile.TemporaryDirectory(prefix="opensportslib-remote-sample-") as temp_dir:
213
+ sample_payload = copy.deepcopy(template)
214
+ sample_payload["data"] = [copy.deepcopy(sample)]
215
+ staged_manifest, archive_path = self._stage_manifest_payload(
216
+ sample_payload,
217
+ manifest_path.parent,
218
+ Path(temp_dir),
219
+ )
220
+ response = self._post_multipart(
221
+ "/predict",
222
+ fields={
223
+ "task_type": task_type,
224
+ "model_id": model_id or self.remote_model_id or "",
225
+ "task_options": json.dumps(task_options or {}),
226
+ },
227
+ files={"test_set_file": staged_manifest, "media_archive": archive_path},
228
+ )
229
+ record.update({"job_id": response.get("job_id"), "status": response.get("status", "queued")})
230
+ logging.info("Submitted remote sample job | sample_id=%s job_id=%s", sample_id, record["job_id"])
231
+ except Exception as exc:
232
+ record.update({"status": "submission_failed", "error": str(exc)})
233
+ submission_errors.append(dict(record))
234
+ logging.error("Remote sample submission failed | sample_id=%s error=%s", sample_id, exc)
235
+ jobs.append(record)
236
+
237
+ return {
238
+ "task_type": task_type,
239
+ "source_manifest": str(manifest_path),
240
+ "remote_mode": "per_sample",
241
+ "manifest_template": template,
242
+ "jobs": jobs,
243
+ "submission_errors": submission_errors,
244
+ }
245
+
246
+ def submit_video_inference(
247
+ self,
248
+ *,
249
+ task_type: str,
250
+ video_path: str,
251
+ question: str | None = None,
252
+ session_id: str | None = None,
253
+ model_id: str | None = None,
254
+ task_options: dict[str, Any] | None = None,
255
+ ) -> dict[str, Any]:
256
+ """Submit a single uploaded video, primarily for direct VQA inference."""
257
+
258
+ if not self.remote:
259
+ raise RuntimeError("Remote inference is not configured. Pass `remote=` to the model constructor.")
260
+ source = Path(expand(video_path)).resolve()
261
+ if not source.is_file():
262
+ raise FileNotFoundError(f"Video file not found: {source}")
263
+ logging.info(
264
+ "Submitting remote video inference | task=%s video=%s video_bytes=%d",
265
+ task_type,
266
+ source,
267
+ source.stat().st_size,
268
+ )
269
+ fields = {
270
+ "task_type": task_type,
271
+ "model_id": model_id or self.remote_model_id or "",
272
+ "task_options": json.dumps(task_options or {}),
273
+ }
274
+ if question is not None:
275
+ fields["question"] = question
276
+ if session_id is not None:
277
+ fields["session_id"] = session_id
278
+ return self._post_multipart("/predict", fields=fields, files={"upload_file": source})
279
+
280
+ def get_remote_job(self, job_id: str) -> dict[str, Any]:
281
+ return self._request_json(f"/jobs/{job_id}")
282
+
283
+ def get_remote_result(self, job_id: str) -> dict[str, Any]:
284
+ return self._request_json(f"/jobs/{job_id}/result")
285
+
286
+ def wait_for_remote_result(self, job_id: str, timeout: float | None = None) -> dict[str, Any]:
287
+ """Poll a submitted remote job until it succeeds, fails, or times out."""
288
+
289
+ deadline = time.monotonic() + (self.remote_result_timeout if timeout is None else float(timeout))
290
+ while True:
291
+ status = self.get_remote_job(job_id)
292
+ state = str(status.get("status", "")).lower()
293
+ logging.info("Remote job status | job_id=%s status=%s", job_id, state or "unknown")
294
+ if state == "succeeded":
295
+ return self.get_remote_result(job_id)
296
+ if state == "failed":
297
+ raise RuntimeError(f"Remote inference job `{job_id}` failed: {status.get('error', 'unknown error')}")
298
+ if time.monotonic() >= deadline:
299
+ raise TimeoutError(f"Timed out waiting for remote inference job `{job_id}`.")
300
+ time.sleep(self.remote_poll_interval)
301
+
302
+ def wait_for_remote_batch(self, batch: dict[str, Any], timeout: float | None = None) -> dict[str, Any]:
303
+ """Collect a per-sample batch without discarding successful sample predictions."""
304
+
305
+ if batch.get("remote_mode") != "per_sample":
306
+ raise ValueError("`wait_for_remote_batch()` requires a `remote_mode=per_sample` submission payload.")
307
+ deadline = time.monotonic() + (self.remote_result_timeout if timeout is None else float(timeout))
308
+ pending = [record for record in batch.get("jobs", []) if record.get("job_id")]
309
+ completed: dict[str, dict[str, Any]] = {}
310
+ failures = list(batch.get("submission_errors") or [])
311
+
312
+ while pending:
313
+ next_pending = []
314
+ for record in pending:
315
+ job_id = str(record["job_id"])
316
+ try:
317
+ status = self.get_remote_job(job_id)
318
+ state = str(status.get("status", "")).lower()
319
+ logging.info("Remote batch job status | sample_id=%s job_id=%s status=%s", record.get("sample_id"), job_id, state)
320
+ if state == "succeeded":
321
+ completed[job_id] = self.get_remote_result(job_id)
322
+ elif state == "failed":
323
+ failures.append({**record, "error": status.get("error", "Remote job failed.")})
324
+ else:
325
+ next_pending.append(record)
326
+ except Exception as exc:
327
+ failures.append({**record, "error": str(exc)})
328
+ pending = next_pending
329
+ if pending:
330
+ if time.monotonic() >= deadline:
331
+ failures.extend({**record, "error": "Timed out waiting for remote job."} for record in pending)
332
+ break
333
+ time.sleep(self.remote_poll_interval)
334
+
335
+ predictions = copy.deepcopy(batch.get("manifest_template") or {})
336
+ predictions["data"] = []
337
+ for record in batch.get("jobs", []):
338
+ result = completed.get(str(record.get("job_id")))
339
+ if result is None:
340
+ continue
341
+ result_payload = (result.get("result") or {}).get("predictions") or {}
342
+ predictions["data"].extend(result_payload.get("data") or [])
343
+ return {
344
+ "task_type": batch.get("task_type"),
345
+ "source_manifest": batch.get("source_manifest"),
346
+ "predictions": predictions,
347
+ "failures": failures,
348
+ "jobs": batch.get("jobs") or [],
349
+ }
350
+
351
+ def _stage_manifest_upload(self, manifest_path: Path, temp_dir: Path) -> tuple[Path, Path]:
352
+ manifest_path, payload = self._load_remote_manifest(manifest_path)
353
+ return self._stage_manifest_payload(payload, manifest_path.parent, temp_dir)
354
+
355
+ def _load_remote_manifest(self, test_set: str | Path) -> tuple[Path, dict[str, Any]]:
356
+ manifest_path = Path(expand(str(test_set))).resolve()
357
+ if not manifest_path.is_file():
358
+ raise FileNotFoundError(f"Test manifest not found: {manifest_path}")
359
+ if manifest_path.suffix.lower() != ".json":
360
+ raise ValueError("Remote test-set uploads currently require a JSON manifest.")
361
+ payload = json.loads(manifest_path.read_text(encoding="utf-8"))
362
+ if not isinstance(payload, dict) or not isinstance(payload.get("data"), list):
363
+ raise ValueError("Test manifest must be an OSL JSON object with a `data` list.")
364
+ return manifest_path, payload
365
+
366
+ def _stage_manifest_payload(
367
+ self,
368
+ payload: dict[str, Any],
369
+ source_root: Path,
370
+ temp_dir: Path,
371
+ ) -> tuple[Path, Path]:
372
+ temp_dir.mkdir(parents=True, exist_ok=True)
373
+ payload = copy.deepcopy(payload)
374
+
375
+ files: dict[Path, str] = {}
376
+ for value_ref in _manifest_media_references(payload):
377
+ raw_path = str(value_ref[0])
378
+ source = Path(raw_path)
379
+ if not source.is_absolute():
380
+ source = source_root / source
381
+ source = source.resolve()
382
+ if not source.is_file():
383
+ raise FileNotFoundError(f"Manifest media file not found: {source}")
384
+ archive_name = files.setdefault(source, f"media/{len(files):05d}_{source.name}")
385
+ value_ref[1](archive_name)
386
+
387
+ staged_manifest = temp_dir / "test_set.json"
388
+ staged_manifest.write_text(json.dumps(payload), encoding="utf-8")
389
+ archive_path = temp_dir / "media.zip"
390
+ with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
391
+ for source, archive_name in files.items():
392
+ archive.write(source, archive_name)
393
+ logging.info(
394
+ "Prepared remote test-set upload | manifest=%s media_files=%d archive_bytes=%d",
395
+ staged_manifest,
396
+ len(files),
397
+ archive_path.stat().st_size,
398
+ )
399
+ return staged_manifest, archive_path
400
+
401
+ def _request_json(self, endpoint: str) -> dict[str, Any]:
402
+ if not self.remote:
403
+ raise RuntimeError("Remote inference is not configured. Pass `remote=` to the model constructor.")
404
+ request = urlrequest.Request(f"{self.remote}{endpoint}", method="GET")
405
+ return self._open_request(request)
406
+
407
+ def _post_multipart(self, endpoint: str, *, fields: dict[str, str], files: dict[str, Path]) -> dict[str, Any]:
408
+ boundary = f"----OpenSportsLib{uuid.uuid4().hex}"
409
+ chunks: list[bytes] = []
410
+ for name, value in fields.items():
411
+ if value == "":
412
+ continue
413
+ chunks.extend((
414
+ f"--{boundary}\r\n".encode(),
415
+ f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode(),
416
+ value.encode(),
417
+ b"\r\n",
418
+ ))
419
+ for name, path in files.items():
420
+ chunks.extend((
421
+ f"--{boundary}\r\n".encode(),
422
+ f'Content-Disposition: form-data; name="{name}"; filename="{path.name}"\r\n'.encode(),
423
+ b"Content-Type: application/octet-stream\r\n\r\n",
424
+ path.read_bytes(),
425
+ b"\r\n",
426
+ ))
427
+ chunks.append(f"--{boundary}--\r\n".encode())
428
+ request = urlrequest.Request(
429
+ f"{self.remote}{endpoint}",
430
+ data=b"".join(chunks),
431
+ method="POST",
432
+ headers={
433
+ "Content-Type": f"multipart/form-data; boundary={boundary}",
434
+ "Content-Length": str(sum(len(chunk) for chunk in chunks)),
435
+ },
436
+ )
437
+ return self._open_request(request)
438
+
439
+ def _open_request(self, request: urlrequest.Request) -> dict[str, Any]:
440
+ started_at = time.monotonic()
441
+ content_length = request.headers.get("Content-length", "0")
442
+ logging.info(
443
+ "Remote server request | method=%s url=%s content_bytes=%s",
444
+ request.get_method(),
445
+ request.full_url,
446
+ content_length,
447
+ )
448
+ try:
449
+ with urlrequest.urlopen(request, timeout=self.remote_timeout) as response:
450
+ body = response.read()
451
+ logging.info(
452
+ "Remote server response | method=%s url=%s status=%s response_bytes=%d elapsed_s=%.2f",
453
+ request.get_method(),
454
+ request.full_url,
455
+ response.status,
456
+ len(body),
457
+ time.monotonic() - started_at,
458
+ )
459
+ return json.loads(body.decode("utf-8"))
460
+ except urlerror.HTTPError as exc:
461
+ detail = exc.read().decode("utf-8", errors="replace")
462
+ logging.error(
463
+ "Remote server error | method=%s url=%s status=%s elapsed_s=%.2f detail=%s",
464
+ request.get_method(),
465
+ request.full_url,
466
+ exc.code,
467
+ time.monotonic() - started_at,
468
+ detail,
469
+ )
470
+ raise RuntimeError(f"Remote server returned HTTP {exc.code}: {detail}") from exc
471
+ except urlerror.URLError as exc:
472
+ logging.error(
473
+ "Remote server connection error | method=%s url=%s elapsed_s=%.2f reason=%s",
474
+ request.get_method(),
475
+ request.full_url,
476
+ time.monotonic() - started_at,
477
+ exc.reason,
478
+ )
479
+ raise ConnectionError(f"Could not reach remote server `{self.remote}`: {exc.reason}") from exc
480
+
481
+ @staticmethod
482
+ def _configure_logging() -> None:
483
+ root_logger = logging.getLogger()
484
+ if not root_logger.handlers:
485
+ logging.basicConfig(
486
+ level=logging.INFO,
487
+ format="%(asctime)s | %(levelname)s | %(message)s",
488
+ )
489
+ elif root_logger.level > logging.INFO:
490
+ root_logger.setLevel(logging.INFO)
491
+
492
+ @abstractmethod
493
+ def load_weights(
494
+ self,
495
+ weights: str | None = None,
496
+ **kwargs,
497
+ ) -> None:
498
+ raise NotImplementedError
499
+
500
+ @abstractmethod
501
+ def train(
502
+ self,
503
+ train_set: str | None = None,
504
+ valid_set: str | None = None,
505
+ weights: str | None = None,
506
+ use_wandb: bool = True,
507
+ **kwargs,
508
+ ) -> str | None:
509
+ raise NotImplementedError
510
+
511
+ @abstractmethod
512
+ def infer(
513
+ self,
514
+ test_set: str | None = None,
515
+ weights: str | None = None,
516
+ use_wandb: bool = True,
517
+ **kwargs,
518
+ ) -> dict:
519
+ raise NotImplementedError
520
+
521
+ @abstractmethod
522
+ def evaluate(
523
+ self,
524
+ test_set: str | None = None,
525
+ weights: str | None = None,
526
+ predictions: str | dict[str, Any] | None = None,
527
+ use_wandb: bool = True,
528
+ **kwargs,
529
+ ) -> dict | str | None:
530
+ raise NotImplementedError
531
+
532
+ def save_predictions(
533
+ self,
534
+ output_path: str,
535
+ predictions: dict,
536
+ ) -> str:
537
+ """Persist in-memory prediction JSON payload to a target file path."""
538
+
539
+ dst = expand(output_path)
540
+ os.makedirs(os.path.dirname(dst) or ".", exist_ok=True)
541
+
542
+ if not isinstance(predictions, dict):
543
+ raise TypeError(
544
+ f"Unsupported predictions type: {type(predictions).__name__}. "
545
+ "Expected dict."
546
+ )
547
+
548
+ with open(dst, "w", encoding="utf-8") as f:
549
+ json.dump(predictions, f)
550
+ return dst
@@ -260,6 +260,38 @@ class ClassificationModel(BaseTaskModel):
260
260
  **kwargs,
261
261
  ):
262
262
  """Run model inference and return predictions in OSL JSON format."""
263
+ remote_mode_provided = "remote_mode" in kwargs
264
+ remote_mode = kwargs.pop("remote_mode", "full_test_set")
265
+ if self.is_remote:
266
+ remote_model_id = kwargs.pop("remote_model_id", None)
267
+ remote_task_options = kwargs.pop("remote_task_options", None)
268
+ if kwargs:
269
+ raise TypeError(f"Unsupported remote inference options: {', '.join(kwargs)}")
270
+ test_set = self._resolve_split_path("test", test_set)
271
+ if remote_mode == "per_sample":
272
+ batch = self.submit_per_sample_inference(
273
+ task_type="classification",
274
+ test_set=test_set,
275
+ model_id=remote_model_id,
276
+ task_options=remote_task_options,
277
+ )
278
+ collected = self.wait_for_remote_batch(batch)
279
+ self.last_remote_failures = collected["failures"]
280
+ if self.last_remote_failures:
281
+ logging.warning("Remote per-sample inference completed with %d failures.", len(self.last_remote_failures))
282
+ return collected["predictions"]
283
+ if remote_mode != "full_test_set":
284
+ raise ValueError("`remote_mode` must be `full_test_set` or `per_sample`.")
285
+ job = self.submit_inference(
286
+ task_type="classification",
287
+ test_set=test_set,
288
+ model_id=remote_model_id,
289
+ task_options=remote_task_options,
290
+ )
291
+ self.last_remote_failures = []
292
+ return self.wait_for_remote_result(job["job_id"])["result"]["predictions"]
293
+ if remote_mode_provided:
294
+ raise ValueError("`remote_mode` is available only when `remote` is configured.")
263
295
  del kwargs
264
296
 
265
297
  import torch