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