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