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.
- {opensportslib-0.2.0.dev7/opensportslib.egg-info → opensportslib-0.3.0.dev2}/PKG-INFO +1 -1
- opensportslib-0.3.0.dev2/opensportslib/tools/__init__.py +76 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/tools/hf_transfer.py +40 -4
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2/opensportslib.egg-info}/PKG-INFO +1 -1
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/pyproject.toml +1 -1
- opensportslib-0.3.0.dev2/tools/convert/build_sn_vqa_2026_vqa.py +524 -0
- opensportslib-0.3.0.dev2/tools/convert/build_soccernet_gar_vqa.py +97 -0
- opensportslib-0.2.0.dev7/opensportslib/tools/__init__.py +0 -52
- opensportslib-0.2.0.dev7/tools/convert/build_sn_vqa_2026_vqa.py +0 -7
- opensportslib-0.2.0.dev7/tools/convert/build_soccernet_gar_vqa.py +0 -7
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/LICENSE +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/LICENSE-COMMERCIAL +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/MANIFEST.in +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/README.md +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/examples/quickstart/basic_classification.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/examples/quickstart/basic_localization.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/examples/quickstart/basic_vqa.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/apis/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/apis/base_task_model.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/apis/classification.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/apis/localization.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/apis/vqa.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/cli.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/classification/default.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/classification/sngar_frames.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/classification/sngar_tracking.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/classification/video.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/default.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/calf_resnetpca512.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/default.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/h5_header_distance.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/h5_header_distance_angle.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/h5_header_distance_speed.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/h5_header_distance_speed_angle.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/h5_header_rule.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/netvladpp_resnetpca512.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/video_dali.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/localization/video_ocv.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/vqa/default.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/vqa/qwen.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/vqa/qwen3_vl_native.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/vqa/qwen_lora.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/vqa/qwen_sngar_frames.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/configs/vqa/xvars.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/accessors.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/conflicts.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/loader.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/migrate.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/migrations/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/migrations/legacy_to_canonical.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/runtime_adapter.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/schema.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/schemas/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/schemas/schema_canonical.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/schemas/schema_legacy.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/config/validate.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/loss/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/loss/builder.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/loss/calf.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/loss/ce.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/loss/combine.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/loss/nll.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/optimizer/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/optimizer/builder.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/sampler/weighted_sampler.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/scheduler/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/scheduler/builder.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/trainer/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/trainer/classification_trainer.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/trainer/localization_trainer.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/trainer/vqa_trainer.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/checkpoint.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/config.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/config_normalize.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/data.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/ddp.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/default_args.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/hf_runtime.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/lightning.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/load_annotations.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/seed.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/video_processing.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/core/utils/wandb.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/builder.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/classification_dataset.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/localization_dataset.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/utils/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/utils/h5_tracking.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/utils/tracking.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/datasets/vqa_dataset.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/classification.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/localization-e2e-ocv.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/localization-json_calf_resnetpca512.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/localization-json_netvlad++_resnetpca512.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/localization.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/sngar-frames.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/legacy_config/sngar-tracking.yaml +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/metrics/classification_metric.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/metrics/localization_metric.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/metrics/vqa_metric.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/backbones/builder.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/contextaware.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/e2e.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/learnablepooling.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/qwen_vl_native.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/qwen_xvars.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/rule_based.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/tracking.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/vars.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/video.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/video_chatgpt_compat.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/video_mae.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/base/xvars_videochatgpt.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/builder.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/heads/builder.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/neck/builder.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/common.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/impl/__init__.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/impl/asformer.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/impl/calf.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/impl/gsm.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/impl/gtad.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/impl/tsm.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/litebase.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/modules.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/shift.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/utils.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/vqa_prediction_priors.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/vqa_prompting.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/models/utils/xvars_clip_index.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/setup/setup.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/tools/_common.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/tools/osl_json_to_parquet.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib/tools/parquet_to_osl_json.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib.egg-info/SOURCES.txt +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib.egg-info/dependency_links.txt +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib.egg-info/entry_points.txt +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib.egg-info/requires.txt +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/opensportslib.egg-info/top_level.txt +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/scripts/run_h5_header_rule_inference.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/setup.cfg +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/conftest.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_classification_dataset_paths.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_classification_trainer_dataloader.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_config_architecture.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_config_split_override_sync.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_config_utils_smoke.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_conversion_tools.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_extract_xvars_features.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_h5_header_rule_spotter.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_h5_tracking_dataset.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_hf_transfer_tools.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_localization_dali_filenames.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_package_smoke.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_pretrained_config_merge_policy.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_public_apis_smoke.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_setup_cli.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_subset_train_infer_integration.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_task_model_api_contract.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_vqa_api.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_vqa_metrics_semantic.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_vqa_qwen_xvars.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_vqa_training_lora.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tests/test_vqa_xvars_videochatgpt.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/convert/build_soccernet_gar.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/convert/build_soccernet_gar_action_spotting.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/convert/build_xvars_indexes.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/convert/extract_xvars_clip_features.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/convert/osl_json_to_parquet_webdataset.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/convert/parquet_webdataset_to_osl_json.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/download/download_hf_repo.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/download/download_osl_hf.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/download/upload_osl_hf.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/training/classification.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/training/localization.py +0 -0
- {opensportslib-0.2.0.dev7 → opensportslib-0.3.0.dev2}/tools/training/vqa.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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()
|