opensportslib 0.3.0.dev21__tar.gz → 0.3.1.dev1__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.0.dev21/opensportslib.egg-info → opensportslib-0.3.1.dev1}/PKG-INFO +1 -1
  2. opensportslib-0.3.1.dev1/opensportslib/apis/base_task_model.py +550 -0
  3. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/apis/classification.py +32 -0
  4. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/apis/localization.py +32 -0
  5. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/apis/vqa.py +62 -0
  6. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/vqa/qwen3_vl_native.yaml +6 -0
  7. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/datasets/vqa_dataset.py +16 -11
  8. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1/opensportslib.egg-info}/PKG-INFO +1 -1
  9. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/pyproject.toml +1 -1
  10. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_task_model_api_contract.py +12 -3
  11. opensportslib-0.3.0.dev21/opensportslib/apis/base_task_model.py +0 -154
  12. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/LICENSE +0 -0
  13. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/LICENSE-COMMERCIAL +0 -0
  14. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/MANIFEST.in +0 -0
  15. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/README.md +0 -0
  16. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/examples/quickstart/basic_classification.py +0 -0
  17. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/examples/quickstart/basic_localization.py +0 -0
  18. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/examples/quickstart/basic_vqa.py +0 -0
  19. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/__init__.py +0 -0
  20. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/adaptation/__init__.py +0 -0
  21. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/adaptation/spotta.py +0 -0
  22. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/apis/__init__.py +0 -0
  23. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/cli.py +0 -0
  24. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/classification/default.yaml +0 -0
  25. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/classification/sngar_frames.yaml +0 -0
  26. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/classification/sngar_tracking.yaml +0 -0
  27. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/classification/video.yaml +0 -0
  28. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/default.yaml +0 -0
  29. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/localization/calf_resnetpca512.yaml +0 -0
  30. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/localization/default.yaml +0 -0
  31. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/localization/e2e_spotta.yaml +0 -0
  32. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/localization/h5_header_distance.yaml +0 -0
  33. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/localization/h5_header_skeleton.yaml +0 -0
  34. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/localization/netvladpp_resnetpca512.yaml +0 -0
  35. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/localization/tracking_action_spotting.yaml +0 -0
  36. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/localization/video_dali.yaml +0 -0
  37. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/localization/video_ocv.yaml +0 -0
  38. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/vqa/default.yaml +0 -0
  39. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/vqa/qwen.yaml +0 -0
  40. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/vqa/qwen_lora.yaml +0 -0
  41. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/vqa/qwen_sngar_frames.yaml +0 -0
  42. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/configs/vqa/xvars.yaml +0 -0
  43. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/__init__.py +0 -0
  44. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/__init__.py +0 -0
  45. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/accessors.py +0 -0
  46. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/conflicts.py +0 -0
  47. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/loader.py +0 -0
  48. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/migrate.py +0 -0
  49. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/migrations/__init__.py +0 -0
  50. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/migrations/legacy_to_canonical.py +0 -0
  51. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/runtime_adapter.py +0 -0
  52. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/schema.py +0 -0
  53. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/schemas/__init__.py +0 -0
  54. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/schemas/schema_canonical.py +0 -0
  55. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/schemas/schema_legacy.py +0 -0
  56. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/config/validate.py +0 -0
  57. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/loss/__init__.py +0 -0
  58. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/loss/builder.py +0 -0
  59. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/loss/calf.py +0 -0
  60. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/loss/ce.py +0 -0
  61. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/loss/combine.py +0 -0
  62. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/loss/nll.py +0 -0
  63. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/optimizer/__init__.py +0 -0
  64. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/optimizer/builder.py +0 -0
  65. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/sampler/weighted_sampler.py +0 -0
  66. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/scheduler/__init__.py +0 -0
  67. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/scheduler/builder.py +0 -0
  68. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/trainer/__init__.py +0 -0
  69. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/trainer/classification_trainer.py +0 -0
  70. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/trainer/localization_trainer.py +0 -0
  71. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/trainer/vqa_trainer.py +0 -0
  72. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/utils/checkpoint.py +0 -0
  73. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/utils/config.py +0 -0
  74. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/utils/config_normalize.py +0 -0
  75. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/utils/data.py +0 -0
  76. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/utils/ddp.py +0 -0
  77. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/utils/default_args.py +0 -0
  78. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/utils/hf_runtime.py +0 -0
  79. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/utils/lightning.py +0 -0
  80. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/utils/load_annotations.py +0 -0
  81. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/utils/seed.py +0 -0
  82. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/utils/video_processing.py +0 -0
  83. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/core/utils/wandb.py +0 -0
  84. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/datasets/__init__.py +0 -0
  85. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/datasets/builder.py +0 -0
  86. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/datasets/classification_dataset.py +0 -0
  87. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/datasets/localization_dataset.py +0 -0
  88. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/datasets/utils/__init__.py +0 -0
  89. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/datasets/utils/h5_tracking.py +0 -0
  90. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/datasets/utils/tracking.py +0 -0
  91. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/legacy_config/classification.yaml +0 -0
  92. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/legacy_config/localization-e2e-ocv.yaml +0 -0
  93. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/legacy_config/localization-json_calf_resnetpca512.yaml +0 -0
  94. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/legacy_config/localization-json_netvlad++_resnetpca512.yaml +0 -0
  95. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/legacy_config/localization.yaml +0 -0
  96. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/legacy_config/sngar-frames.yaml +0 -0
  97. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/legacy_config/sngar-tracking.yaml +0 -0
  98. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/metrics/classification_metric.py +0 -0
  99. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/metrics/localization_metric.py +0 -0
  100. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/metrics/vqa_metric.py +0 -0
  101. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/__init__.py +0 -0
  102. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/backbones/builder.py +0 -0
  103. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/base/contextaware.py +0 -0
  104. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/base/e2e.py +0 -0
  105. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/base/learnablepooling.py +0 -0
  106. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/base/qwen_vl_native.py +0 -0
  107. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/base/qwen_xvars.py +0 -0
  108. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/base/rule_based.py +0 -0
  109. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/base/tracking.py +0 -0
  110. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/base/vars.py +0 -0
  111. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/base/video.py +0 -0
  112. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/base/video_chatgpt_compat.py +0 -0
  113. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/base/video_mae.py +0 -0
  114. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/base/xvars_videochatgpt.py +0 -0
  115. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/builder.py +0 -0
  116. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/heads/builder.py +0 -0
  117. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/neck/builder.py +0 -0
  118. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/common.py +0 -0
  119. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/impl/__init__.py +0 -0
  120. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/impl/asformer.py +0 -0
  121. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/impl/calf.py +0 -0
  122. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/impl/gsm.py +0 -0
  123. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/impl/gtad.py +0 -0
  124. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/impl/tsm.py +0 -0
  125. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/litebase.py +0 -0
  126. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/modules.py +0 -0
  127. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/shift.py +0 -0
  128. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/utils.py +0 -0
  129. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/vqa_prediction_priors.py +0 -0
  130. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/vqa_prompting.py +0 -0
  131. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/models/utils/xvars_clip_index.py +0 -0
  132. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/setup/setup.py +0 -0
  133. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/tools/__init__.py +0 -0
  134. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/tools/_common.py +0 -0
  135. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/tools/hf_transfer.py +0 -0
  136. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/tools/osl_json_to_parquet.py +0 -0
  137. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib/tools/parquet_to_osl_json.py +0 -0
  138. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib.egg-info/SOURCES.txt +0 -0
  139. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib.egg-info/dependency_links.txt +0 -0
  140. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib.egg-info/entry_points.txt +0 -0
  141. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib.egg-info/requires.txt +0 -0
  142. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/opensportslib.egg-info/top_level.txt +0 -0
  143. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/scripts/run_h5_header_rule_inference.py +0 -0
  144. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/setup.cfg +0 -0
  145. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/conftest.py +0 -0
  146. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/release/__init__.py +0 -0
  147. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/release/_release_common.py +0 -0
  148. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/release/test_classification_release.py +0 -0
  149. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/release/test_localization_release.py +0 -0
  150. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/release/test_vqa_release.py +0 -0
  151. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_classification_dataset_paths.py +0 -0
  152. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_classification_trainer_dataloader.py +0 -0
  153. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_config_architecture.py +0 -0
  154. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_config_split_override_sync.py +0 -0
  155. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_config_utils_smoke.py +0 -0
  156. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_conversion_tools.py +0 -0
  157. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_extract_xvars_features.py +0 -0
  158. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_h5_header_rule_spotter.py +0 -0
  159. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_h5_header_skeleton_spotter.py +0 -0
  160. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_h5_tracking_dataset.py +0 -0
  161. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_hf_transfer_tools.py +0 -0
  162. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_localization_dali_filenames.py +0 -0
  163. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_localization_hf_backend_override.py +0 -0
  164. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_localization_intervals.py +0 -0
  165. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_package_smoke.py +0 -0
  166. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_pretrained_config_merge_policy.py +0 -0
  167. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_public_apis_smoke.py +0 -0
  168. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_setup_cli.py +0 -0
  169. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_spotta_e2e.py +0 -0
  170. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_subset_train_infer_integration.py +0 -0
  171. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_vqa_api.py +0 -0
  172. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_vqa_metrics_semantic.py +0 -0
  173. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_vqa_qwen_xvars.py +0 -0
  174. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_vqa_training_lora.py +0 -0
  175. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tests/test_vqa_xvars_videochatgpt.py +0 -0
  176. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/convert/build_sn_vqa_2026_vqa.py +0 -0
  177. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/convert/build_sngar_spotting.py +0 -0
  178. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/convert/build_soccernet_gar.py +0 -0
  179. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/convert/build_soccernet_gar_action_spotting.py +0 -0
  180. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/convert/build_soccernet_gar_vqa.py +0 -0
  181. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/convert/build_xvars_indexes.py +0 -0
  182. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/convert/extract_xvars_clip_features.py +0 -0
  183. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/convert/osl_json_to_parquet_webdataset.py +0 -0
  184. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/convert/parquet_webdataset_to_osl_json.py +0 -0
  185. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/convert/sngar_dataset_card.py +0 -0
  186. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/convert/sngar_events.py +0 -0
  187. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/convert/verify_sngar_spotting.py +0 -0
  188. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/download/download_hf_repo.py +0 -0
  189. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/download/download_osl_hf.py +0 -0
  190. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/download/push_sngar_spotting.py +0 -0
  191. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/download/upload_osl_hf.py +0 -0
  192. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/training/classification.py +0 -0
  193. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/training/localization.py +0 -0
  194. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/tools/training/vqa.py +0 -0
  195. {opensportslib-0.3.0.dev21 → opensportslib-0.3.1.dev1}/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.0.dev21
3
+ Version: 0.3.1.dev1
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
@@ -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
@@ -426,6 +426,38 @@ class LocalizationModel(BaseTaskModel):
426
426
  **kwargs,
427
427
  ):
428
428
  """Run model inference and return predictions in OSL JSON format."""
429
+ remote_mode_provided = "remote_mode" in kwargs
430
+ remote_mode = kwargs.pop("remote_mode", "full_test_set")
431
+ if self.is_remote:
432
+ remote_model_id = kwargs.pop("remote_model_id", None)
433
+ remote_task_options = kwargs.pop("remote_task_options", None)
434
+ if kwargs:
435
+ raise TypeError(f"Unsupported remote inference options: {', '.join(kwargs)}")
436
+ test_set = self._resolve_split_path("test", test_set)
437
+ if remote_mode == "per_sample":
438
+ batch = self.submit_per_sample_inference(
439
+ task_type="localization",
440
+ test_set=test_set,
441
+ model_id=remote_model_id,
442
+ task_options=remote_task_options,
443
+ )
444
+ collected = self.wait_for_remote_batch(batch)
445
+ self.last_remote_failures = collected["failures"]
446
+ if self.last_remote_failures:
447
+ logging.warning("Remote per-sample inference completed with %d failures.", len(self.last_remote_failures))
448
+ return collected["predictions"]
449
+ if remote_mode != "full_test_set":
450
+ raise ValueError("`remote_mode` must be `full_test_set` or `per_sample`.")
451
+ job = self.submit_inference(
452
+ task_type="localization",
453
+ test_set=test_set,
454
+ model_id=remote_model_id,
455
+ task_options=remote_task_options,
456
+ )
457
+ self.last_remote_failures = []
458
+ return self.wait_for_remote_result(job["job_id"])["result"]["predictions"]
459
+ if remote_mode_provided:
460
+ raise ValueError("`remote_mode` is available only when `remote` is configured.")
429
461
  from opensportslib.datasets.builder import build_dataset
430
462
  from opensportslib.models.builder import build_model
431
463
  from opensportslib.core.trainer.localization_trainer import build_inferer