opensportslib 0.2.0.dev7__tar.gz → 0.3.0.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 (182) hide show
  1. {opensportslib-0.2.0.dev7/opensportslib.egg-info → opensportslib-0.3.0.dev2}/PKG-INFO +1 -1
  2. opensportslib-0.3.0.dev2/opensportslib/tools/__init__.py +76 -0
  3. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/tools/hf_transfer.py +40 -4
  4. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2/opensportslib.egg-info}/PKG-INFO +1 -1
  5. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/pyproject.toml +1 -1
  6. opensportslib-0.3.0.dev2/tools/convert/build_sn_vqa_2026_vqa.py +524 -0
  7. opensportslib-0.3.0.dev2/tools/convert/build_soccernet_gar_vqa.py +97 -0
  8. opensportslib-0.2.0.dev7/opensportslib/tools/__init__.py +0 -52
  9. opensportslib-0.2.0.dev7/tools/convert/build_sn_vqa_2026_vqa.py +0 -7
  10. opensportslib-0.2.0.dev7/tools/convert/build_soccernet_gar_vqa.py +0 -7
  11. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/LICENSE +0 -0
  12. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/LICENSE-COMMERCIAL +0 -0
  13. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/MANIFEST.in +0 -0
  14. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/README.md +0 -0
  15. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/examples/quickstart/basic_classification.py +0 -0
  16. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/examples/quickstart/basic_localization.py +0 -0
  17. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/examples/quickstart/basic_vqa.py +0 -0
  18. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/__init__.py +0 -0
  19. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/apis/__init__.py +0 -0
  20. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/apis/base_task_model.py +0 -0
  21. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/apis/classification.py +0 -0
  22. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/apis/localization.py +0 -0
  23. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/apis/vqa.py +0 -0
  24. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/cli.py +0 -0
  25. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/classification/default.yaml +0 -0
  26. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/classification/sngar_frames.yaml +0 -0
  27. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/classification/sngar_tracking.yaml +0 -0
  28. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/classification/video.yaml +0 -0
  29. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/default.yaml +0 -0
  30. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/calf_resnetpca512.yaml +0 -0
  31. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/default.yaml +0 -0
  32. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/h5_header_distance.yaml +0 -0
  33. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/h5_header_distance_angle.yaml +0 -0
  34. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/h5_header_distance_speed.yaml +0 -0
  35. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/h5_header_distance_speed_angle.yaml +0 -0
  36. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/h5_header_rule.yaml +0 -0
  37. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/netvladpp_resnetpca512.yaml +0 -0
  38. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/video_dali.yaml +0 -0
  39. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/video_ocv.yaml +0 -0
  40. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/vqa/default.yaml +0 -0
  41. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/vqa/qwen.yaml +0 -0
  42. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/vqa/qwen3_vl_native.yaml +0 -0
  43. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/vqa/qwen_lora.yaml +0 -0
  44. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/vqa/qwen_sngar_frames.yaml +0 -0
  45. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/vqa/xvars.yaml +0 -0
  46. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/__init__.py +0 -0
  47. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/__init__.py +0 -0
  48. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/accessors.py +0 -0
  49. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/conflicts.py +0 -0
  50. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/loader.py +0 -0
  51. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/migrate.py +0 -0
  52. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/migrations/__init__.py +0 -0
  53. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/migrations/legacy_to_canonical.py +0 -0
  54. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/runtime_adapter.py +0 -0
  55. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/schema.py +0 -0
  56. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/schemas/__init__.py +0 -0
  57. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/schemas/schema_canonical.py +0 -0
  58. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/schemas/schema_legacy.py +0 -0
  59. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/validate.py +0 -0
  60. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/loss/__init__.py +0 -0
  61. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/loss/builder.py +0 -0
  62. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/loss/calf.py +0 -0
  63. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/loss/ce.py +0 -0
  64. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/loss/combine.py +0 -0
  65. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/loss/nll.py +0 -0
  66. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/optimizer/__init__.py +0 -0
  67. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/optimizer/builder.py +0 -0
  68. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/sampler/weighted_sampler.py +0 -0
  69. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/scheduler/__init__.py +0 -0
  70. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/scheduler/builder.py +0 -0
  71. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/trainer/__init__.py +0 -0
  72. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/trainer/classification_trainer.py +0 -0
  73. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/trainer/localization_trainer.py +0 -0
  74. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/trainer/vqa_trainer.py +0 -0
  75. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/checkpoint.py +0 -0
  76. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/config.py +0 -0
  77. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/config_normalize.py +0 -0
  78. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/data.py +0 -0
  79. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/ddp.py +0 -0
  80. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/default_args.py +0 -0
  81. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/hf_runtime.py +0 -0
  82. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/lightning.py +0 -0
  83. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/load_annotations.py +0 -0
  84. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/seed.py +0 -0
  85. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/video_processing.py +0 -0
  86. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/wandb.py +0 -0
  87. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/__init__.py +0 -0
  88. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/builder.py +0 -0
  89. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/classification_dataset.py +0 -0
  90. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/localization_dataset.py +0 -0
  91. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/utils/__init__.py +0 -0
  92. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/utils/h5_tracking.py +0 -0
  93. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/utils/tracking.py +0 -0
  94. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/vqa_dataset.py +0 -0
  95. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/classification.yaml +0 -0
  96. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/localization-e2e-ocv.yaml +0 -0
  97. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/localization-json_calf_resnetpca512.yaml +0 -0
  98. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/localization-json_netvlad++_resnetpca512.yaml +0 -0
  99. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/localization.yaml +0 -0
  100. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/sngar-frames.yaml +0 -0
  101. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/sngar-tracking.yaml +0 -0
  102. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/metrics/classification_metric.py +0 -0
  103. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/metrics/localization_metric.py +0 -0
  104. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/metrics/vqa_metric.py +0 -0
  105. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/__init__.py +0 -0
  106. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/backbones/builder.py +0 -0
  107. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/contextaware.py +0 -0
  108. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/e2e.py +0 -0
  109. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/learnablepooling.py +0 -0
  110. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/qwen_vl_native.py +0 -0
  111. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/qwen_xvars.py +0 -0
  112. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/rule_based.py +0 -0
  113. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/tracking.py +0 -0
  114. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/vars.py +0 -0
  115. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/video.py +0 -0
  116. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/video_chatgpt_compat.py +0 -0
  117. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/video_mae.py +0 -0
  118. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/xvars_videochatgpt.py +0 -0
  119. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/builder.py +0 -0
  120. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/heads/builder.py +0 -0
  121. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/neck/builder.py +0 -0
  122. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/common.py +0 -0
  123. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/impl/__init__.py +0 -0
  124. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/impl/asformer.py +0 -0
  125. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/impl/calf.py +0 -0
  126. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/impl/gsm.py +0 -0
  127. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/impl/gtad.py +0 -0
  128. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/impl/tsm.py +0 -0
  129. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/litebase.py +0 -0
  130. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/modules.py +0 -0
  131. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/shift.py +0 -0
  132. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/utils.py +0 -0
  133. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/vqa_prediction_priors.py +0 -0
  134. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/vqa_prompting.py +0 -0
  135. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/xvars_clip_index.py +0 -0
  136. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/setup/setup.py +0 -0
  137. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/tools/_common.py +0 -0
  138. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/tools/osl_json_to_parquet.py +0 -0
  139. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/tools/parquet_to_osl_json.py +0 -0
  140. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib.egg-info/SOURCES.txt +0 -0
  141. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib.egg-info/dependency_links.txt +0 -0
  142. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib.egg-info/entry_points.txt +0 -0
  143. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib.egg-info/requires.txt +0 -0
  144. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib.egg-info/top_level.txt +0 -0
  145. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/scripts/run_h5_header_rule_inference.py +0 -0
  146. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/setup.cfg +0 -0
  147. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/conftest.py +0 -0
  148. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_classification_dataset_paths.py +0 -0
  149. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_classification_trainer_dataloader.py +0 -0
  150. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_config_architecture.py +0 -0
  151. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_config_split_override_sync.py +0 -0
  152. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_config_utils_smoke.py +0 -0
  153. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_conversion_tools.py +0 -0
  154. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_extract_xvars_features.py +0 -0
  155. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_h5_header_rule_spotter.py +0 -0
  156. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_h5_tracking_dataset.py +0 -0
  157. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_hf_transfer_tools.py +0 -0
  158. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_localization_dali_filenames.py +0 -0
  159. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_package_smoke.py +0 -0
  160. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_pretrained_config_merge_policy.py +0 -0
  161. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_public_apis_smoke.py +0 -0
  162. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_setup_cli.py +0 -0
  163. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_subset_train_infer_integration.py +0 -0
  164. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_task_model_api_contract.py +0 -0
  165. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_vqa_api.py +0 -0
  166. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_vqa_metrics_semantic.py +0 -0
  167. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_vqa_qwen_xvars.py +0 -0
  168. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_vqa_training_lora.py +0 -0
  169. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_vqa_xvars_videochatgpt.py +0 -0
  170. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/convert/build_soccernet_gar.py +0 -0
  171. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/convert/build_soccernet_gar_action_spotting.py +0 -0
  172. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/convert/build_xvars_indexes.py +0 -0
  173. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/convert/extract_xvars_clip_features.py +0 -0
  174. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/convert/osl_json_to_parquet_webdataset.py +0 -0
  175. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/convert/parquet_webdataset_to_osl_json.py +0 -0
  176. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/download/download_hf_repo.py +0 -0
  177. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/download/download_osl_hf.py +0 -0
  178. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/download/upload_osl_hf.py +0 -0
  179. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/training/classification.py +0 -0
  180. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/training/localization.py +0 -0
  181. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/training/vqa.py +0 -0
  182. {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.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.2.0.dev7
3
+ Version: 0.3.0.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
@@ -0,0 +1,76 @@
1
+ """OpenSportsLib dataset conversion and Hugging Face transfer tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+
7
+
8
+ _EXPORTS = {
9
+ "convert_json_to_parquet": ("opensportslib.tools.osl_json_to_parquet", "convert_json_to_parquet"),
10
+ "DEFAULT_SHARD_SIZE": ("opensportslib.tools.osl_json_to_parquet", "DEFAULT_SHARD_SIZE"),
11
+ "parse_shard_size": ("opensportslib.tools.osl_json_to_parquet", "parse_shard_size"),
12
+ "convert_parquet_to_json": ("opensportslib.tools.parquet_to_osl_json", "convert_parquet_to_json"),
13
+ "convert_sn_vqa_2026_to_osl": ("opensportslib.tools.sn_vqa_2026_qwen_native", "convert_sn_vqa_2026_to_osl"),
14
+ "evaluate_sn_vqa_predictions": ("opensportslib.tools.sn_vqa_2026_qwen_native", "evaluate_sn_vqa_predictions"),
15
+ "HfTransferCancelled": ("opensportslib.tools.hf_transfer", "HfTransferCancelled"),
16
+ "HF_REPO_ID_KEY": ("opensportslib.tools.hf_transfer", "HF_REPO_ID_KEY"),
17
+ "HF_BRANCH_KEY": ("opensportslib.tools.hf_transfer", "HF_BRANCH_KEY"),
18
+ "HF_SPLIT_KEY": ("opensportslib.tools.hf_transfer", "HF_SPLIT_KEY"),
19
+ "download_dataset_split_from_hf": ("opensportslib.tools.hf_transfer", "download_dataset_split_from_hf"),
20
+ "upload_dataset_inputs_from_json_to_hf": ("opensportslib.tools.hf_transfer", "upload_dataset_inputs_from_json_to_hf"),
21
+ "upload_dataset_as_parquet_to_hf": ("opensportslib.tools.hf_transfer", "upload_dataset_as_parquet_to_hf"),
22
+ "create_dataset_repo_on_hf": ("opensportslib.tools.hf_transfer", "create_dataset_repo_on_hf"),
23
+ "dataset_repo_exists_on_hf": ("opensportslib.tools.hf_transfer", "dataset_repo_exists_on_hf"),
24
+ "create_dataset_branch_on_hf": ("opensportslib.tools.hf_transfer", "create_dataset_branch_on_hf"),
25
+ "read_hf_source_metadata_from_dataset": ("opensportslib.tools.hf_transfer", "read_hf_source_metadata_from_dataset"),
26
+ "write_hf_source_metadata_to_dataset_json": ("opensportslib.tools.hf_transfer", "write_hf_source_metadata_to_dataset_json"),
27
+ "is_hf_repo_not_found_error": ("opensportslib.tools.hf_transfer", "is_hf_repo_not_found_error"),
28
+ "is_hf_revision_not_found_error": ("opensportslib.tools.hf_transfer", "is_hf_revision_not_found_error"),
29
+ "is_hf_download_url_not_found_error": ("opensportslib.tools.hf_transfer", "is_hf_download_url_not_found_error"),
30
+ "get_json_repo_folder": ("opensportslib.tools.hf_transfer", "get_json_repo_folder"),
31
+ "extract_repo_paths_from_json": ("opensportslib.tools.hf_transfer", "extract_repo_paths_from_json"),
32
+ "extract_local_input_upload_entries_from_json": ("opensportslib.tools.hf_transfer", "extract_local_input_upload_entries_from_json"),
33
+ }
34
+
35
+ __all__ = [
36
+ "convert_json_to_parquet",
37
+ "DEFAULT_SHARD_SIZE",
38
+ "parse_shard_size",
39
+ "convert_parquet_to_json",
40
+ "convert_sn_vqa_2026_to_osl",
41
+ "evaluate_sn_vqa_predictions",
42
+ "HfTransferCancelled",
43
+ "HF_REPO_ID_KEY",
44
+ "HF_BRANCH_KEY",
45
+ "HF_SPLIT_KEY",
46
+ "download_dataset_split_from_hf",
47
+ "upload_dataset_inputs_from_json_to_hf",
48
+ "upload_dataset_as_parquet_to_hf",
49
+ "create_dataset_repo_on_hf",
50
+ "dataset_repo_exists_on_hf",
51
+ "create_dataset_branch_on_hf",
52
+ "read_hf_source_metadata_from_dataset",
53
+ "write_hf_source_metadata_to_dataset_json",
54
+ "is_hf_repo_not_found_error",
55
+ "is_hf_revision_not_found_error",
56
+ "is_hf_download_url_not_found_error",
57
+ "get_json_repo_folder",
58
+ "extract_repo_paths_from_json",
59
+ "extract_local_input_upload_entries_from_json",
60
+ ]
61
+
62
+
63
+ def __getattr__(name: str):
64
+ try:
65
+ module_name, attr_name = _EXPORTS[name]
66
+ except KeyError as exc:
67
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from exc
68
+
69
+ module = importlib.import_module(module_name)
70
+ value = getattr(module, attr_name)
71
+ globals()[name] = value
72
+ return value
73
+
74
+
75
+ def __dir__() -> list[str]:
76
+ return sorted(set(globals()) | set(__all__))
@@ -5,22 +5,58 @@ import tempfile
5
5
  from pathlib import Path
6
6
  from typing import Any, Callable
7
7
 
8
- from .osl_json_to_parquet import DEFAULT_SHARD_SIZE, convert_json_to_parquet, parse_shard_size
9
- from .parquet_to_osl_json import convert_parquet_to_json
10
-
11
-
12
8
  ProgressCallback = Callable[[str], None]
13
9
  CancelCheck = Callable[[], bool]
14
10
 
15
11
  HF_REPO_ID_KEY = "hf_repo_id"
16
12
  HF_BRANCH_KEY = "hf_branch"
17
13
  HF_SPLIT_KEY = "hf_split"
14
+ DEFAULT_SHARD_SIZE = 1_000_000_000
18
15
 
19
16
 
20
17
  class HfTransferCancelled(RuntimeError):
21
18
  pass
22
19
 
23
20
 
21
+ def _import_osl_json_to_parquet():
22
+ try:
23
+ from .osl_json_to_parquet import DEFAULT_SHARD_SIZE as module_default_shard_size
24
+ from .osl_json_to_parquet import convert_json_to_parquet as module_convert_json_to_parquet
25
+ from .osl_json_to_parquet import parse_shard_size as module_parse_shard_size
26
+ except ImportError as exc:
27
+ raise RuntimeError(
28
+ "Missing conversion dependencies for OSL JSON -> Parquet tools. "
29
+ "Install the package with its data-conversion dependencies, including 'pandas' and 'pyarrow'."
30
+ ) from exc
31
+ return module_default_shard_size, module_convert_json_to_parquet, module_parse_shard_size
32
+
33
+
34
+ def _import_parquet_to_osl_json():
35
+ try:
36
+ from .parquet_to_osl_json import convert_parquet_to_json as module_convert_parquet_to_json
37
+ except ImportError as exc:
38
+ raise RuntimeError(
39
+ "Missing conversion dependencies for Parquet -> OSL JSON tools. "
40
+ "Install the package with its data-conversion dependencies, including 'pandas' and 'pyarrow'."
41
+ ) from exc
42
+ return module_convert_parquet_to_json
43
+
44
+
45
+ def parse_shard_size(value: int | str) -> int:
46
+ _, _, module_parse_shard_size = _import_osl_json_to_parquet()
47
+ return module_parse_shard_size(value)
48
+
49
+
50
+ def convert_json_to_parquet(*args, **kwargs):
51
+ _, module_convert_json_to_parquet, _ = _import_osl_json_to_parquet()
52
+ return module_convert_json_to_parquet(*args, **kwargs)
53
+
54
+
55
+ def convert_parquet_to_json(*args, **kwargs):
56
+ module_convert_parquet_to_json = _import_parquet_to_osl_json()
57
+ return module_convert_parquet_to_json(*args, **kwargs)
58
+
59
+
24
60
  def _emit_progress(progress_cb: ProgressCallback | None, message: str) -> None:
25
61
  if progress_cb:
26
62
  progress_cb(message)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opensportslib
3
- Version: 0.2.0.dev7
3
+ Version: 0.3.0.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "opensportslib"
7
- version = "0.2.0.dev7"
7
+ version = "0.3.0.dev2"
8
8
  description = "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."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -0,0 +1,524 @@
1
+ """Convert SoccerNet SN-VQA-2026 test rows into native-Qwen-runnable OSL JSON."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import re
9
+ from collections import Counter, defaultdict
10
+ from dataclasses import dataclass
11
+ from datetime import date
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".bmp", ".webp"}
16
+ VIDEO_SUFFIXES = {".mp4", ".avi", ".mov", ".mkv", ".webm", ".m4v"}
17
+ DEFAULT_CONVERSION_DATE = "2026-07-30"
18
+
19
+
20
+ @dataclass
21
+ class MaterialInfo:
22
+ path: Path
23
+ relative_path: str
24
+ kind: str
25
+
26
+
27
+ def _require_media_runtime():
28
+ try:
29
+ import cv2 # type: ignore
30
+ import numpy as np # type: ignore
31
+ except ImportError as exc:
32
+ raise ImportError(
33
+ "SN-VQA-2026 native-Qwen conversion requires both 'numpy' and 'opencv-python'."
34
+ ) from exc
35
+ return cv2, np
36
+
37
+
38
+ def _load_json(path: Path) -> Any:
39
+ with path.open(encoding="utf-8") as f:
40
+ return json.load(f)
41
+
42
+
43
+ def _dump_json(path: Path, payload: dict[str, Any]) -> None:
44
+ path.parent.mkdir(parents=True, exist_ok=True)
45
+ with path.open("w", encoding="utf-8") as f:
46
+ json.dump(payload, f, indent=2, ensure_ascii=False)
47
+
48
+
49
+ def _normalize_answer_text(text: str) -> str:
50
+ trimmed = str(text or "").strip()
51
+ cleaned = trimmed.strip(" \t\r\n.,;:!?\"'`()[]{}")
52
+ cleaned = re.sub(r"\s+", " ", cleaned).strip().upper()
53
+ return cleaned
54
+
55
+
56
+ def _normalize_answer_alias(text: str) -> str:
57
+ return re.sub(r"[^A-Z0-9]+", "", _normalize_answer_text(text))
58
+
59
+
60
+ def _find_unique_substring_label(answer_text: str, allowed_labels: list[str]) -> str | None:
61
+ normalized_answer = _normalize_answer_text(answer_text)
62
+ if not normalized_answer:
63
+ return None
64
+ hits: list[tuple[int, str]] = []
65
+ for label in allowed_labels:
66
+ normalized_label = _normalize_answer_text(label)
67
+ if not normalized_label:
68
+ continue
69
+ if normalized_label in normalized_answer:
70
+ hits.append((len(normalized_label), label))
71
+ continue
72
+ alias = _normalize_answer_alias(label)
73
+ if alias and alias in _normalize_answer_alias(answer_text):
74
+ hits.append((len(alias), label))
75
+ if not hits:
76
+ return None
77
+ hits.sort(reverse=True)
78
+ best_len = hits[0][0]
79
+ best = {label for length, label in hits if length == best_len}
80
+ if len(best) == 1:
81
+ return next(iter(best))
82
+ return None
83
+
84
+
85
+ def _find_unique_token_cover_label(answer_text: str, allowed_labels: list[str]) -> str | None:
86
+ answer_tokens = set(re.findall(r"[A-Z0-9]+", _normalize_answer_text(answer_text)))
87
+ if not answer_tokens:
88
+ return None
89
+ matches: list[tuple[int, str]] = []
90
+ for label in allowed_labels:
91
+ label_tokens = re.findall(r"[A-Z0-9]+", _normalize_answer_text(label))
92
+ if not label_tokens:
93
+ continue
94
+ if len(label_tokens) > 5:
95
+ continue
96
+ if all(token in answer_tokens for token in label_tokens):
97
+ matches.append((len(label_tokens), label))
98
+ if not matches:
99
+ return None
100
+ matches.sort(reverse=True)
101
+ best_len = matches[0][0]
102
+ best = {label for length, label in matches if length == best_len}
103
+ if len(best) == 1:
104
+ return next(iter(best))
105
+ return None
106
+
107
+
108
+ def normalize_prediction_to_option(answer_text: str, allowed_labels: list[str]) -> str | None:
109
+ labels = [str(label).strip() for label in (allowed_labels or []) if str(label).strip()]
110
+ if not labels:
111
+ return None
112
+ normalized = _normalize_answer_text(answer_text)
113
+ if not normalized:
114
+ return None
115
+ exact_map = {_normalize_answer_text(label): label for label in labels}
116
+ predicted = exact_map.get(normalized)
117
+ if predicted is not None:
118
+ return predicted
119
+ alias_map = {_normalize_answer_alias(label): label for label in labels}
120
+ predicted = alias_map.get(_normalize_answer_alias(answer_text))
121
+ if predicted is not None:
122
+ return predicted
123
+ predicted = _find_unique_substring_label(answer_text, labels)
124
+ if predicted is not None:
125
+ return predicted
126
+ return _find_unique_token_cover_label(answer_text, labels)
127
+
128
+
129
+ def _classify_material(path: str) -> str:
130
+ suffix = Path(path).suffix.lower()
131
+ if suffix in IMAGE_SUFFIXES:
132
+ return "image"
133
+ if suffix in VIDEO_SUFFIXES:
134
+ return "video"
135
+ return "unknown"
136
+
137
+
138
+ def _sample_video_frames(video_path: Path, num_frames: int) -> list[Any]:
139
+ cv2, np = _require_media_runtime()
140
+ capture = cv2.VideoCapture(str(video_path))
141
+ if not capture.isOpened():
142
+ raise ValueError(f"Could not open video: {video_path}")
143
+ try:
144
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
145
+ if frame_count <= 0:
146
+ raise ValueError(f"Video has no readable frames: {video_path}")
147
+ indices = np.linspace(0, max(frame_count - 1, 0), num=max(1, int(num_frames)), dtype=int)
148
+ frames: list[np.ndarray] = []
149
+ for idx in indices:
150
+ capture.set(cv2.CAP_PROP_POS_FRAMES, int(idx))
151
+ ok, frame = capture.read()
152
+ if not ok or frame is None:
153
+ continue
154
+ frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
155
+ if not frames:
156
+ raise ValueError(f"Could not sample frames from video: {video_path}")
157
+ return frames
158
+ finally:
159
+ capture.release()
160
+
161
+
162
+ def _load_image(path: Path) -> Any:
163
+ cv2, _ = _require_media_runtime()
164
+ image = cv2.imread(str(path))
165
+ if image is None:
166
+ raise ValueError(f"Could not read image: {path}")
167
+ return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
168
+
169
+
170
+ def _resolve_materials(
171
+ materials: Any,
172
+ *,
173
+ dataset_root: Path,
174
+ split: str,
175
+ ) -> tuple[str, list[MaterialInfo], list[str]]:
176
+ if materials is None:
177
+ return "null", [], []
178
+ if not isinstance(materials, list) or not materials:
179
+ return "empty", [], []
180
+
181
+ infos: list[MaterialInfo] = []
182
+ missing: list[str] = []
183
+ has_image = False
184
+ has_video = False
185
+ for rel in materials:
186
+ rel_path = str(rel)
187
+ full_path = dataset_root / split / rel_path
188
+ if not full_path.exists():
189
+ missing.append(rel_path)
190
+ continue
191
+ kind = _classify_material(rel_path)
192
+ if kind == "image":
193
+ has_image = True
194
+ elif kind == "video":
195
+ has_video = True
196
+ infos.append(MaterialInfo(path=full_path, relative_path=f"{split}/{rel_path}", kind=kind))
197
+
198
+ if has_image and has_video:
199
+ category = "mixed_media"
200
+ elif has_video and len(infos) == 1:
201
+ category = "single_video"
202
+ elif has_video:
203
+ category = "multi_video"
204
+ elif has_image and len(infos) == 1:
205
+ category = "single_image"
206
+ elif has_image:
207
+ category = "multi_image"
208
+ else:
209
+ category = "unknown"
210
+ return category, infos, missing
211
+
212
+
213
+ def _build_choice_map(row: dict[str, Any]) -> dict[str, str]:
214
+ out = {}
215
+ for key in ("O1", "O2", "O3", "O4"):
216
+ value = str(row.get(key) or "").strip()
217
+ if value:
218
+ out[key] = value
219
+ return out
220
+
221
+
222
+ def _fuse_materials(
223
+ sample_id: str,
224
+ materials: list[MaterialInfo],
225
+ *,
226
+ fused_dir: Path,
227
+ frames_per_video: int,
228
+ ) -> str:
229
+ _, np = _require_media_runtime()
230
+ frames: list[Any] = []
231
+ for item in materials:
232
+ if item.kind == "video":
233
+ frames.extend(_sample_video_frames(item.path, frames_per_video))
234
+ elif item.kind == "image":
235
+ frames.append(_load_image(item.path))
236
+ if not frames:
237
+ raise ValueError(f"No usable frames produced for sample '{sample_id}'")
238
+ fused_dir.mkdir(parents=True, exist_ok=True)
239
+ array = np.stack(frames, axis=0).astype(np.uint8, copy=False)
240
+ fused_path = fused_dir / f"{sample_id}.npy"
241
+ np.save(fused_path, array)
242
+ return str(fused_path)
243
+
244
+
245
+ def convert_sn_vqa_2026_to_osl(
246
+ *,
247
+ dataset_root: str,
248
+ split: str = "test",
249
+ output_manifest: str | None = None,
250
+ output_report: str | None = None,
251
+ fused_dir: str | None = None,
252
+ frames_per_video: int = 4,
253
+ conversion_date: str = DEFAULT_CONVERSION_DATE,
254
+ ) -> dict[str, Any]:
255
+ root = Path(dataset_root).expanduser().resolve()
256
+ split_dir = root / split
257
+ source_json = split_dir / f"{split}.json"
258
+ rows = _load_json(source_json)
259
+ if not isinstance(rows, list):
260
+ raise ValueError(f"Expected list payload in {source_json}")
261
+
262
+ fused_root = Path(fused_dir).expanduser().resolve() if fused_dir else (split_dir / "fused_qwen_native")
263
+ manifest_path = Path(output_manifest).expanduser().resolve() if output_manifest else (root / f"{split}_osl_qwen_native.json")
264
+ report_path = Path(output_report).expanduser().resolve() if output_report else (root / f"{split}_conversion_report.json")
265
+
266
+ data: list[dict[str, Any]] = []
267
+ excluded_ids: dict[str, list[str]] = defaultdict(list)
268
+ category_counts: Counter[str] = Counter()
269
+ fused_artifact_count = 0
270
+
271
+ for row in rows:
272
+ sample_id = str(row.get("id"))
273
+ question = str(row.get("Q") or "").strip()
274
+ open_answer = str(row.get("openA") or "").strip()
275
+ choice_map = _build_choice_map(row)
276
+ allowed_labels = [choice_map[key] for key in ("O1", "O2", "O3", "O4") if key in choice_map]
277
+ correct_key = str(row.get("closeA") or "").strip()
278
+ correct_option_text = choice_map.get(correct_key, "")
279
+
280
+ category, materials, missing = _resolve_materials(row.get("materials"), dataset_root=root, split=split)
281
+ category_counts[category] += 1
282
+ if category == "null":
283
+ excluded_ids["null_materials"].append(sample_id)
284
+ continue
285
+ if category == "empty":
286
+ excluded_ids["empty_materials"].append(sample_id)
287
+ continue
288
+ if missing:
289
+ excluded_ids["missing_files"].append(sample_id)
290
+ continue
291
+ if not question:
292
+ excluded_ids["missing_question"].append(sample_id)
293
+ continue
294
+ if not allowed_labels:
295
+ excluded_ids["missing_options"].append(sample_id)
296
+ continue
297
+ if any(item.kind == "unknown" for item in materials):
298
+ excluded_ids["unsupported_material_type"].append(sample_id)
299
+ continue
300
+
301
+ sample = {
302
+ "id": sample_id,
303
+ "question": question,
304
+ "references": [open_answer] if open_answer else [],
305
+ "ground_truth_label": open_answer,
306
+ "allowed_labels": allowed_labels,
307
+ "metadata": {
308
+ "source_dataset": "SN-VQA-2026",
309
+ "source_row_id": row.get("id"),
310
+ "correct_option_key": correct_key,
311
+ "correct_option_text": correct_option_text,
312
+ "choice_map": choice_map,
313
+ "original_materials": row.get("materials"),
314
+ },
315
+ }
316
+
317
+ try:
318
+ if category == "single_image":
319
+ sample["frame_paths"] = [materials[0].relative_path]
320
+ elif category == "multi_image":
321
+ sample["frame_paths"] = [item.relative_path for item in materials]
322
+ elif category == "single_video":
323
+ sample["video_path"] = materials[0].relative_path
324
+ elif category in {"multi_video", "mixed_media"}:
325
+ fused_path = _fuse_materials(sample_id, materials, fused_dir=fused_root, frames_per_video=frames_per_video)
326
+ sample["video_path"] = os.path.relpath(fused_path, root)
327
+ fused_artifact_count += 1
328
+ else:
329
+ excluded_ids["unsupported_material_layout"].append(sample_id)
330
+ continue
331
+ except Exception:
332
+ excluded_ids["fusion_or_decode_error"].append(sample_id)
333
+ continue
334
+
335
+ data.append(sample)
336
+
337
+ manifest = {
338
+ "version": "2.0",
339
+ "date": conversion_date,
340
+ "dataset_name": "SN-VQA-2026-test-qwen-native",
341
+ "description": "Runnable SN-VQA-2026 test conversion for OpenSportsLib native Qwen VL inference.",
342
+ "modalities": ["image", "video"],
343
+ "metadata": {
344
+ "source_dataset": "SN-VQA-2026",
345
+ "split": split,
346
+ "conversion_date": conversion_date,
347
+ "source_annotation": str(source_json),
348
+ "frames_per_video_for_fusion": int(frames_per_video),
349
+ },
350
+ "data": data,
351
+ }
352
+ report = {
353
+ "dataset_name": "SN-VQA-2026-test-qwen-native",
354
+ "split": split,
355
+ "conversion_date": conversion_date,
356
+ "source_row_count": len(rows),
357
+ "runnable_row_count": len(data),
358
+ "excluded_row_count": len(rows) - len(data),
359
+ "excluded_ids_by_reason": {
360
+ key: sorted(value, key=lambda item: int(item) if str(item).isdigit() else str(item))
361
+ for key, value in sorted(excluded_ids.items())
362
+ },
363
+ "material_category_counts": dict(category_counts),
364
+ "fused_artifact_count": fused_artifact_count,
365
+ "generated_manifest": str(manifest_path),
366
+ "generated_fused_dir": str(fused_root),
367
+ }
368
+
369
+ _dump_json(manifest_path, manifest)
370
+ _dump_json(report_path, report)
371
+ return {
372
+ "manifest_path": str(manifest_path),
373
+ "report_path": str(report_path),
374
+ "fused_dir": str(fused_root),
375
+ "manifest": manifest,
376
+ "report": report,
377
+ }
378
+
379
+
380
+ def evaluate_sn_vqa_predictions(
381
+ *,
382
+ manifest_path: str,
383
+ predictions_path: str,
384
+ output_path: str | None = None,
385
+ ) -> dict[str, Any]:
386
+ manifest = _load_json(Path(manifest_path).expanduser().resolve())
387
+ predictions = _load_json(Path(predictions_path).expanduser().resolve())
388
+ manifest_rows = {str(row.get("id")): row for row in manifest.get("data", [])}
389
+ prediction_rows = list(predictions.get("data", []))
390
+
391
+ evaluated_rows = []
392
+ matched_count = 0
393
+ option_key_correct = 0
394
+ text_exact_match = 0
395
+ unresolved = 0
396
+
397
+ for row in prediction_rows:
398
+ sample_id = str(row.get("id"))
399
+ source = manifest_rows.get(sample_id, {})
400
+ allowed_labels = list(source.get("allowed_labels") or [])
401
+ metadata = dict(source.get("metadata") or {})
402
+ choice_map = dict(metadata.get("choice_map") or {})
403
+ answer_text = str(row.get("answer_text") or "")
404
+ predicted_label = str(row.get("predicted_label") or "").strip() or normalize_prediction_to_option(answer_text, allowed_labels)
405
+ predicted_option_key = None
406
+ for key, value in choice_map.items():
407
+ if predicted_label and str(value).strip() == predicted_label:
408
+ predicted_option_key = key
409
+ break
410
+ correct_option_key = str(metadata.get("correct_option_key") or "").strip() or None
411
+ correct_text = str(source.get("ground_truth_label") or "").strip()
412
+ is_unresolved = predicted_option_key is None
413
+ unresolved += int(is_unresolved)
414
+ if predicted_label:
415
+ matched_count += 1
416
+ if predicted_option_key and correct_option_key and predicted_option_key == correct_option_key:
417
+ option_key_correct += 1
418
+ if _normalize_answer_text(answer_text) == _normalize_answer_text(correct_text):
419
+ text_exact_match += 1
420
+ enriched = dict(row)
421
+ enriched["predicted_label"] = predicted_label
422
+ enriched["predicted_option_key"] = predicted_option_key
423
+ enriched["correct_option_key"] = correct_option_key
424
+ enriched["correct_option_text"] = metadata.get("correct_option_text")
425
+ enriched["text_exact_match_openA"] = _normalize_answer_text(answer_text) == _normalize_answer_text(correct_text)
426
+ enriched["option_key_correct_closeA"] = bool(
427
+ predicted_option_key and correct_option_key and predicted_option_key == correct_option_key
428
+ )
429
+ enriched["is_unresolved"] = is_unresolved
430
+ evaluated_rows.append(enriched)
431
+
432
+ total = len(prediction_rows)
433
+ result = {
434
+ "dataset_name": manifest.get("dataset_name"),
435
+ "manifest_path": str(Path(manifest_path).expanduser().resolve()),
436
+ "predictions_path": str(Path(predictions_path).expanduser().resolve()),
437
+ "evaluated_at": date.today().isoformat(),
438
+ "summary": {
439
+ "prediction_count": total,
440
+ "resolved_prediction_count": matched_count,
441
+ "unresolved_prediction_count": unresolved,
442
+ "unresolved_rate": (unresolved / total) if total else 0.0,
443
+ "text_exact_match_openA": (text_exact_match / total) if total else 0.0,
444
+ "option_key_accuracy_closeA": (option_key_correct / total) if total else 0.0,
445
+ },
446
+ "data": evaluated_rows,
447
+ }
448
+ if output_path:
449
+ _dump_json(Path(output_path).expanduser().resolve(), result)
450
+ return result
451
+
452
+
453
+ def _parse_args() -> argparse.Namespace:
454
+ parser = argparse.ArgumentParser(description=__doc__)
455
+ subparsers = parser.add_subparsers(dest="command", required=True)
456
+
457
+ convert_parser = subparsers.add_parser("convert", help="Convert SoccerNet test rows into runnable OSL JSON.")
458
+ convert_parser.add_argument("--dataset-root", required=True, help="Path to the SN-VQA-2026 dataset root.")
459
+ convert_parser.add_argument("--split", default="test", help="Dataset split to convert.")
460
+ convert_parser.add_argument("--output-manifest", default=None, help="Output OSL manifest path.")
461
+ convert_parser.add_argument("--output-report", default=None, help="Output conversion report path.")
462
+ convert_parser.add_argument("--fused-dir", default=None, help="Directory for fused .npy assets.")
463
+ convert_parser.add_argument(
464
+ "--frames-per-video",
465
+ type=int,
466
+ default=4,
467
+ help="Frames sampled from each video during fusion.",
468
+ )
469
+ convert_parser.add_argument(
470
+ "--conversion-date",
471
+ default=DEFAULT_CONVERSION_DATE,
472
+ help="Date string to record in metadata.",
473
+ )
474
+
475
+ eval_parser = subparsers.add_parser(
476
+ "evaluate-predictions",
477
+ help="Map predictions back to SoccerNet option keys.",
478
+ )
479
+ eval_parser.add_argument("--manifest", required=True, help="Converted runnable manifest path.")
480
+ eval_parser.add_argument(
481
+ "--predictions",
482
+ required=True,
483
+ help="Prediction JSON from VQAModel/Trainer_VQA inference.",
484
+ )
485
+ eval_parser.add_argument("--output", default=None, help="Optional evaluation output JSON.")
486
+ return parser.parse_args()
487
+
488
+
489
+ def main() -> None:
490
+ args = _parse_args()
491
+ if args.command == "convert":
492
+ result = convert_sn_vqa_2026_to_osl(
493
+ dataset_root=args.dataset_root,
494
+ split=args.split,
495
+ output_manifest=args.output_manifest,
496
+ output_report=args.output_report,
497
+ fused_dir=args.fused_dir,
498
+ frames_per_video=args.frames_per_video,
499
+ conversion_date=args.conversion_date,
500
+ )
501
+ print(
502
+ json.dumps(
503
+ {
504
+ "manifest_path": result["manifest_path"],
505
+ "report_path": result["report_path"],
506
+ "runnable_row_count": result["report"]["runnable_row_count"],
507
+ "excluded_row_count": result["report"]["excluded_row_count"],
508
+ "fused_artifact_count": result["report"]["fused_artifact_count"],
509
+ },
510
+ indent=2,
511
+ )
512
+ )
513
+ return
514
+
515
+ result = evaluate_sn_vqa_predictions(
516
+ manifest_path=args.manifest,
517
+ predictions_path=args.predictions,
518
+ output_path=args.output,
519
+ )
520
+ print(json.dumps(result["summary"], indent=2))
521
+
522
+
523
+ if __name__ == "__main__":
524
+ main()