opensportslib 0.2.0.dev3__py3-none-any.whl → 0.2.0.dev4__py3-none-any.whl
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.
- examples/quickstart/basic_vqa.py +37 -0
- opensportslib/configs/vqa/default.yaml +172 -0
- opensportslib/configs/vqa/xvars.yaml +58 -0
- {opensportslib-0.2.0.dev3.dist-info → opensportslib-0.2.0.dev4.dist-info}/METADATA +3 -3
- {opensportslib-0.2.0.dev3.dist-info → opensportslib-0.2.0.dev4.dist-info}/RECORD +13 -7
- tests/test_setup_cli.py +101 -0
- tests/test_vqa_api.py +240 -0
- tests/test_vqa_qwen_xvars.py +251 -0
- {opensportslib-0.2.0.dev3.dist-info → opensportslib-0.2.0.dev4.dist-info}/WHEEL +0 -0
- {opensportslib-0.2.0.dev3.dist-info → opensportslib-0.2.0.dev4.dist-info}/entry_points.txt +0 -0
- {opensportslib-0.2.0.dev3.dist-info → opensportslib-0.2.0.dev4.dist-info}/licenses/LICENSE +0 -0
- {opensportslib-0.2.0.dev3.dist-info → opensportslib-0.2.0.dev4.dist-info}/licenses/LICENSE-COMMERCIAL +0 -0
- {opensportslib-0.2.0.dev3.dist-info → opensportslib-0.2.0.dev4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from opensportslib.apis import VQAModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def main():
|
|
5
|
+
"""
|
|
6
|
+
Minimal VQA example.
|
|
7
|
+
Update config, question, and dataset paths before running.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
my_model = VQAModel(
|
|
11
|
+
config="examples/configs/vqa_qwen.yaml",
|
|
12
|
+
weights=None, # optional: path or Hugging Face model ID
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
predictions = my_model.infer(
|
|
16
|
+
test_set="/path/to/test_annotations.json",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
print(predictions)
|
|
20
|
+
|
|
21
|
+
single_prediction = my_model.infer(
|
|
22
|
+
video_path="/path/to/video.mp4",
|
|
23
|
+
question="What card would you give? Why?",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
print(single_prediction)
|
|
27
|
+
|
|
28
|
+
metrics = my_model.evaluate(
|
|
29
|
+
test_set="/path/to/test_annotations.json",
|
|
30
|
+
predictions=predictions,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
print(metrics)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
TASK: vqa
|
|
2
|
+
VERSION: 2
|
|
3
|
+
|
|
4
|
+
SYSTEM:
|
|
5
|
+
paths:
|
|
6
|
+
save_dir: ./checkpoints_vqa
|
|
7
|
+
work_dir: ${SYSTEM.paths.save_dir}
|
|
8
|
+
device: cuda
|
|
9
|
+
gpu:
|
|
10
|
+
count: 1
|
|
11
|
+
id: 0
|
|
12
|
+
reproducibility:
|
|
13
|
+
use_seed: true
|
|
14
|
+
seed: 42
|
|
15
|
+
|
|
16
|
+
DATA:
|
|
17
|
+
common:
|
|
18
|
+
dataset_name: OSL-XFoul
|
|
19
|
+
data_root: /home/vorajv/dataset/OSL-XFoul
|
|
20
|
+
feature_index: /home/vorajv/dataset/OSL-XFoul/feature_index.json
|
|
21
|
+
prediction_index: /home/vorajv/dataset/OSL-XFoul/prediction_index.json
|
|
22
|
+
runtime:
|
|
23
|
+
loader_backend: opencv
|
|
24
|
+
splits:
|
|
25
|
+
train:
|
|
26
|
+
annotation_path: /home/vorajv/dataset/OSL-XFoul/train.json
|
|
27
|
+
source_path: /home/vorajv/dataset/OSL-XFoul
|
|
28
|
+
dataloader:
|
|
29
|
+
batch_size: 1
|
|
30
|
+
shuffle: true
|
|
31
|
+
num_workers: 0
|
|
32
|
+
pin_memory: false
|
|
33
|
+
mp_context: spawn
|
|
34
|
+
persistent_workers: false
|
|
35
|
+
valid:
|
|
36
|
+
annotation_path: /home/vorajv/dataset/OSL-XFoul/valid.json
|
|
37
|
+
source_path: /home/vorajv/dataset/OSL-XFoul
|
|
38
|
+
dataloader:
|
|
39
|
+
batch_size: 1
|
|
40
|
+
shuffle: false
|
|
41
|
+
num_workers: 0
|
|
42
|
+
pin_memory: false
|
|
43
|
+
mp_context: spawn
|
|
44
|
+
persistent_workers: false
|
|
45
|
+
test:
|
|
46
|
+
annotation_path: /home/vorajv/dataset/OSL-XFoul/test.json
|
|
47
|
+
source_path: /home/vorajv/dataset/OSL-XFoul
|
|
48
|
+
dataloader:
|
|
49
|
+
batch_size: 1
|
|
50
|
+
shuffle: false
|
|
51
|
+
num_workers: 0
|
|
52
|
+
pin_memory: false
|
|
53
|
+
mp_context: spawn
|
|
54
|
+
persistent_workers: false
|
|
55
|
+
inputs:
|
|
56
|
+
video:
|
|
57
|
+
modality: video
|
|
58
|
+
representation: raw
|
|
59
|
+
source:
|
|
60
|
+
format: mp4
|
|
61
|
+
sampling:
|
|
62
|
+
num_frames: 100
|
|
63
|
+
input_fps: 25
|
|
64
|
+
target_fps: 17
|
|
65
|
+
start_frame: 63
|
|
66
|
+
end_frame: 87
|
|
67
|
+
transform: {}
|
|
68
|
+
augmentations: {}
|
|
69
|
+
params: {}
|
|
70
|
+
question:
|
|
71
|
+
modality: text
|
|
72
|
+
representation: raw
|
|
73
|
+
source:
|
|
74
|
+
format: json
|
|
75
|
+
sampling: {}
|
|
76
|
+
transform: {}
|
|
77
|
+
augmentations: {}
|
|
78
|
+
params: {}
|
|
79
|
+
|
|
80
|
+
MODEL:
|
|
81
|
+
runtime:
|
|
82
|
+
dtype: fp16
|
|
83
|
+
device: auto
|
|
84
|
+
compile: false
|
|
85
|
+
freeze: false
|
|
86
|
+
load:
|
|
87
|
+
checkpoint_path: null
|
|
88
|
+
pretrained: false
|
|
89
|
+
strict: true
|
|
90
|
+
map_location: null
|
|
91
|
+
format: auto
|
|
92
|
+
topology:
|
|
93
|
+
- from: video_encoder
|
|
94
|
+
to: mm_projector
|
|
95
|
+
- from: mm_projector
|
|
96
|
+
to: llm_decoder
|
|
97
|
+
|
|
98
|
+
IO:
|
|
99
|
+
inputs:
|
|
100
|
+
video: video_encoder
|
|
101
|
+
question: llm_decoder
|
|
102
|
+
outputs:
|
|
103
|
+
answer_text: llm_decoder
|
|
104
|
+
explanation_text: llm_decoder
|
|
105
|
+
|
|
106
|
+
TRAIN:
|
|
107
|
+
trainer:
|
|
108
|
+
type: vqa
|
|
109
|
+
epochs: 3
|
|
110
|
+
criterion:
|
|
111
|
+
type: CrossEntropyLoss
|
|
112
|
+
optimizer:
|
|
113
|
+
type: AdamW
|
|
114
|
+
lr: 0.0002
|
|
115
|
+
weight_decay: 0.001
|
|
116
|
+
scheduler:
|
|
117
|
+
type: constant
|
|
118
|
+
execution:
|
|
119
|
+
enabled: true
|
|
120
|
+
training_backend: xvars_videochatgpt_lora
|
|
121
|
+
feature_backend: xvars_clip
|
|
122
|
+
view_sampling_policy: random_train_deterministic_eval
|
|
123
|
+
acc_grad_iter: 8
|
|
124
|
+
log_interval: 1
|
|
125
|
+
dry_run: false
|
|
126
|
+
xvars:
|
|
127
|
+
feature_mode: strict_xvars
|
|
128
|
+
projection_path: null
|
|
129
|
+
prompt:
|
|
130
|
+
style: detailed
|
|
131
|
+
include_priors: true
|
|
132
|
+
prediction_prior_adapter: xvars_referee
|
|
133
|
+
prior_fields: [action, offence, contact, bodypart]
|
|
134
|
+
video_token_len: 300
|
|
135
|
+
generation:
|
|
136
|
+
max_new_tokens: 128
|
|
137
|
+
temperature: 0.0
|
|
138
|
+
eval_profile:
|
|
139
|
+
metric_set: [exact_match, contains_match, token_f1, referee_semantic]
|
|
140
|
+
aggregation: mean
|
|
141
|
+
exclusions: []
|
|
142
|
+
sft:
|
|
143
|
+
max_seq_length: 480
|
|
144
|
+
include_video_tokens: true
|
|
145
|
+
disable_tqdm: false
|
|
146
|
+
reference_mode: all
|
|
147
|
+
append_eos_token: true
|
|
148
|
+
gradient_checkpointing: true
|
|
149
|
+
save_strategy: epoch
|
|
150
|
+
lora:
|
|
151
|
+
r: 16
|
|
152
|
+
alpha: 32
|
|
153
|
+
dropout: 0.05
|
|
154
|
+
bias: none
|
|
155
|
+
prepare_kbit: true
|
|
156
|
+
target_modules: [mm_projector, upsample_features, up_proj, down_proj, gate_proj, k_proj, q_proj, v_proj, o_proj]
|
|
157
|
+
exclude_modules: '^base_lm\.model\.mm_projector$'
|
|
158
|
+
quantization:
|
|
159
|
+
enabled: false
|
|
160
|
+
load_in_4bit: true
|
|
161
|
+
bnb_4bit_quant_type: nf4
|
|
162
|
+
compute_dtype: float16
|
|
163
|
+
bnb_4bit_use_double_quant: true
|
|
164
|
+
checkpoint:
|
|
165
|
+
save_adapter: true
|
|
166
|
+
merge_and_save: false
|
|
167
|
+
selection:
|
|
168
|
+
monitor: loss
|
|
169
|
+
mode: min
|
|
170
|
+
checkpoint:
|
|
171
|
+
save_every: 1
|
|
172
|
+
save_best: true
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
SYSTEM:
|
|
2
|
+
paths:
|
|
3
|
+
save_dir: ./checkpoints_vqa_lora
|
|
4
|
+
gpu:
|
|
5
|
+
count: 4
|
|
6
|
+
|
|
7
|
+
MODEL:
|
|
8
|
+
components:
|
|
9
|
+
video_encoder:
|
|
10
|
+
kind: encoder
|
|
11
|
+
source:
|
|
12
|
+
provider: opensportslib
|
|
13
|
+
# XVARS-trained classifier weights to load into that architecture
|
|
14
|
+
load:
|
|
15
|
+
weights_path: /home/vorajv/X-VARS/weights/14_model.pth.tar
|
|
16
|
+
params:
|
|
17
|
+
# CLIP architecture and image processor to instantiate.
|
|
18
|
+
feature_source: indexed_or_raw_clip
|
|
19
|
+
vision_tower: openai/clip-vit-large-patch14
|
|
20
|
+
feature_dim: 1024
|
|
21
|
+
overrides: {}
|
|
22
|
+
mm_projector:
|
|
23
|
+
kind: projector
|
|
24
|
+
source:
|
|
25
|
+
provider: opensportslib
|
|
26
|
+
params:
|
|
27
|
+
input_dim: 1024
|
|
28
|
+
overrides: {}
|
|
29
|
+
llm_decoder:
|
|
30
|
+
kind: decoder
|
|
31
|
+
source:
|
|
32
|
+
provider: opensportslib
|
|
33
|
+
params:
|
|
34
|
+
repo_id: /home/vorajv/X-VARS/weights/base_model_videoChatGPT
|
|
35
|
+
overrides: {}
|
|
36
|
+
metadata:
|
|
37
|
+
backend: xvars_videochatgpt
|
|
38
|
+
|
|
39
|
+
TRAIN:
|
|
40
|
+
execution:
|
|
41
|
+
prompt:
|
|
42
|
+
system_prompt: You are Video-ChatGPT, a large vision-language assistant. You are able to understand the video content that the user provides, and assist the user with a variety of tasks using natural language.Follow the instructions carefully and explain your answers in detail based on the provided video.
|
|
43
|
+
|
|
44
|
+
# Optional XFoul-only generated-answer smoke test.
|
|
45
|
+
# During LoRA training, this runs generation on one known training sample and
|
|
46
|
+
# checks that the answer still contains referee-domain terms and avoids known
|
|
47
|
+
# code-like failure strings. Disable or replace these values for non-XFoul data.
|
|
48
|
+
generated_validation:
|
|
49
|
+
enabled: true
|
|
50
|
+
sample_id: action_0
|
|
51
|
+
every_steps: 25
|
|
52
|
+
max_new_tokens: 128
|
|
53
|
+
require_relevance: true
|
|
54
|
+
required_terms: [foul, card, challenge, spa, dogso, advantage]
|
|
55
|
+
forbidden_terms: [get_children, django, httpclient, "```python", "```php"]
|
|
56
|
+
|
|
57
|
+
hf:
|
|
58
|
+
tokenizer_id: ${MODEL.components.llm_decoder.params.repo_id}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opensportslib
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev4
|
|
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
|
|
@@ -324,8 +324,8 @@ backend currently supports `Qwen/Qwen2.5-7B-Instruct` and
|
|
|
324
324
|
For X-VARS, `feature_source: indexed_or_raw_clip` prefers indexed CLIP features
|
|
325
325
|
when available and falls back to extracting CLIP features from raw video during
|
|
326
326
|
`infer()`. Pre-extracted features remain the preferred path for parity, speed,
|
|
327
|
-
and reproducibility. See [docs/
|
|
328
|
-
|
|
327
|
+
and reproducibility. See [docs/tools/vqa.md](docs/tools/vqa.md) for the full
|
|
328
|
+
VQA setup workflow.
|
|
329
329
|
|
|
330
330
|
|
|
331
331
|
---
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
examples/quickstart/basic_classification.py,sha256=byefVwdVS6yHuHOSBRHB3czrm5ya4ZpIM_qI72UTpv4,1048
|
|
2
2
|
examples/quickstart/basic_localization.py,sha256=ZTwVIBFcIuIcctW9OJNHctpsIKAgtVsRzn2HxmYYLaY,1049
|
|
3
|
+
examples/quickstart/basic_vqa.py,sha256=q8qe5X3J3jHatUcykod4LFhvhEYwwXGFXzg0Qd_oO_o,773
|
|
3
4
|
opensportslib/__init__.py,sha256=yl-0Zd38CgUGwn-ZUpT_rbaJx1nMwUPjod5U1TcAL7U,619
|
|
4
5
|
opensportslib/cli.py,sha256=45tA92cctPQuruJACAm4TqEbdFwLp-AEknvsOgszxUM,829
|
|
5
6
|
opensportslib/apis/__init__.py,sha256=4PCdg5owC2HWoTnPYiTl140ddz1rxipexoOmxHKL2yE,460
|
|
@@ -17,7 +18,9 @@ opensportslib/configs/localization/default.yaml,sha256=CCsR1ewdVn5aBTo6nBPkBnwCt
|
|
|
17
18
|
opensportslib/configs/localization/netvladpp_resnetpca512.yaml,sha256=i-bB-LGekwdha761xuBjIs_6LJpTmQ2MH6Vusc1WSXI,3169
|
|
18
19
|
opensportslib/configs/localization/video_dali.yaml,sha256=GhbqlNLJNY3U2Vwd1TOBtO--yRNpk0wbt8xFoxTibbg,2802
|
|
19
20
|
opensportslib/configs/localization/video_ocv.yaml,sha256=sbUpACassaVK6ND4ChogiK9DfMvJisPM9SUpSh8olvg,3055
|
|
21
|
+
opensportslib/configs/vqa/default.yaml,sha256=wH9eOJIwxZVRvXvgyT-0iTIN7IlN29a_GFTIkKB_9mw,4060
|
|
20
22
|
opensportslib/configs/vqa/qwen.yaml,sha256=yQ7RSCa3JKN3POrgZWjfYdsG_BsQeo0oDekojUe1jqg,1318
|
|
23
|
+
opensportslib/configs/vqa/xvars.yaml,sha256=JA299ps1yQ5upvnX9P-iC7dbvUb4rcBTjGnQQUVRY14,1957
|
|
21
24
|
opensportslib/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
25
|
opensportslib/core/config/__init__.py,sha256=joSTWPBuLQDSFQIbAVpmOyeFLTxhIZRR_-K4DgEK8MM,635
|
|
23
26
|
opensportslib/core/config/accessors.py,sha256=I6oi2lhqr9HChci90iHV3SnJAw6ykxeTLUVoecS-xhk,19613
|
|
@@ -111,8 +114,8 @@ opensportslib/tools/_common.py,sha256=CNSADAK8odNS0xUd2tNExoukrgKEbfNNwGbwIF5JJZ
|
|
|
111
114
|
opensportslib/tools/hf_transfer.py,sha256=wkVJc0iyJojJSV6XBAPtPVQ58NbrGNoEZG5c3-bkMEE,31275
|
|
112
115
|
opensportslib/tools/osl_json_to_parquet.py,sha256=VsEy7KF7x6PA5GPdiAFDgYlyfW9Cey-SvnOiRca2s74,15132
|
|
113
116
|
opensportslib/tools/parquet_to_osl_json.py,sha256=CcD4oYF8HTwJnANr81PerJXLRqTc6BZ6mVsKu7YNJw0,9259
|
|
114
|
-
opensportslib-0.2.0.
|
|
115
|
-
opensportslib-0.2.0.
|
|
117
|
+
opensportslib-0.2.0.dev4.dist-info/licenses/LICENSE,sha256=TfPDBt3ar0uv_f9cqCDMZ5rIzW3CY8anRRd4PkL6ejs,34522
|
|
118
|
+
opensportslib-0.2.0.dev4.dist-info/licenses/LICENSE-COMMERCIAL,sha256=dg9GjCyCFMcfDJPkZ-IFDDt3Z5x_IdTY3YY-yOC1Eo0,163
|
|
116
119
|
tests/conftest.py,sha256=M10-WTFqwR8n4GvaHh2uDzeknxOWSwCoWslpHA4VQ3I,26806
|
|
117
120
|
tests/test_classification_dataset_paths.py,sha256=UMKj52WcWIayPAYVm7ZAmsF2W9yiYecEJ_F7lzmejEs,3636
|
|
118
121
|
tests/test_classification_trainer_dataloader.py,sha256=T-1ukT7UOjnoAG-9zD4oLHijTRy7qD_vAlMFmavU5oI,5118
|
|
@@ -126,9 +129,12 @@ tests/test_localization_dali_filenames.py,sha256=6bUH0UHFXNHsuo93GlYf8vZAjTwnXgw
|
|
|
126
129
|
tests/test_package_smoke.py,sha256=-FDsRc4tdeGJwDMXBJT4Dlcsnsx6LvmeUXq9vnFm4YI,666
|
|
127
130
|
tests/test_pretrained_config_merge_policy.py,sha256=GzCJbxxYfrqTrrUhoWuMVzF5Swb7WmqfruXE0M8UXVo,6677
|
|
128
131
|
tests/test_public_apis_smoke.py,sha256=SJhAbMPQB-UzvTKPOlA-EoYB6pptJdcRnWRU-YhsOZg,1746
|
|
132
|
+
tests/test_setup_cli.py,sha256=1LFyPZZTsf5xvoGyTME0cZ9-Aog_wnSjDFLIIODVVL4,3157
|
|
129
133
|
tests/test_subset_train_infer_integration.py,sha256=ACw-PJRCQO2KCd4gjFBU4cgMb2Ka6MRABai_w-0HqJU,8238
|
|
130
134
|
tests/test_task_model_api_contract.py,sha256=5L5q-eZ2QrL0HhW4EXBihWUevpJ3r3HYbvYuVqLR3A0,32025
|
|
135
|
+
tests/test_vqa_api.py,sha256=7CM8_Is6mDAjldIWVKE8xIVGazclf5R41IJ1s5CG24c,7654
|
|
131
136
|
tests/test_vqa_metrics_semantic.py,sha256=xAiVRuJi21olgqYcxP3p2ekD-EHqiKzkO-6dZAg2yvo,3973
|
|
137
|
+
tests/test_vqa_qwen_xvars.py,sha256=KpifJb5Gr3my86CQ9VVmxV494Nmz2VVy5t7Tid0JFyU,9805
|
|
132
138
|
tests/test_vqa_training_lora.py,sha256=AdA-WevoNkdSaikOTuYnqXYyLL-LcPGiZ95AWMyo2EA,20742
|
|
133
139
|
tests/test_vqa_xvars_videochatgpt.py,sha256=1xCEJRrM0O-CYeQ0GX4rot4WSeEi0eD3w0HTR5LfoPs,50945
|
|
134
140
|
tools/convert/build_soccernet_gar.py,sha256=XJ9esTiYjFkbhmETgucYa4I9Blb39moxyO74iIh3wBE,42540
|
|
@@ -142,8 +148,8 @@ tools/download/download_osl_hf.py,sha256=4dd-Yei2g_lp7NKom8FQlR99PQkQO3DZpAyKmdY
|
|
|
142
148
|
tools/download/upload_osl_hf.py,sha256=YO-1uqxs-_6wdEdVYrJ7nqelpRDUOYsEmZW5HhpseiI,5777
|
|
143
149
|
tools/training/classification.py,sha256=cSMIMNfP08McYUOt17ONo4WVKjAmLDl20WYOidYjJ8k,1277
|
|
144
150
|
tools/training/localization.py,sha256=UC7liIEF_BrVtN8lnLZ5DOGEqSuwB_rNQgjoN_xEKuI,1271
|
|
145
|
-
opensportslib-0.2.0.
|
|
146
|
-
opensportslib-0.2.0.
|
|
147
|
-
opensportslib-0.2.0.
|
|
148
|
-
opensportslib-0.2.0.
|
|
149
|
-
opensportslib-0.2.0.
|
|
151
|
+
opensportslib-0.2.0.dev4.dist-info/METADATA,sha256=fjastTzSf7e0mf-iHBBzfgDYtvLG7zdjNuFLx39VwzE,12842
|
|
152
|
+
opensportslib-0.2.0.dev4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
153
|
+
opensportslib-0.2.0.dev4.dist-info/entry_points.txt,sha256=8haQLjVcah3IRKrlsyeecr0cUB5eXH9UPgJ_DDdT4Zk,57
|
|
154
|
+
opensportslib-0.2.0.dev4.dist-info/top_level.txt,sha256=IJ6LnztNOeuEKQTiMTPPokBJr9ZxUCEIvbCjlPwUVEs,35
|
|
155
|
+
opensportslib-0.2.0.dev4.dist-info/RECORD,,
|
tests/test_setup_cli.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from opensportslib import cli
|
|
4
|
+
from opensportslib.setup import setup as setup_lib
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_cli_setup_forwards_xvars_flag(monkeypatch):
|
|
8
|
+
captured: dict[str, object] = {}
|
|
9
|
+
|
|
10
|
+
def fake_setup(*, dali: bool, pyg: bool, vqa_xvars: bool, vqa_qwen: bool):
|
|
11
|
+
captured["dali"] = dali
|
|
12
|
+
captured["pyg"] = pyg
|
|
13
|
+
captured["vqa_xvars"] = vqa_xvars
|
|
14
|
+
captured["vqa_qwen"] = vqa_qwen
|
|
15
|
+
|
|
16
|
+
monkeypatch.setattr(cli, "setup", fake_setup)
|
|
17
|
+
|
|
18
|
+
rc = cli.main(["setup", "--vqa_xvars", "--dali"])
|
|
19
|
+
|
|
20
|
+
assert rc == 0
|
|
21
|
+
assert captured == {"dali": True, "pyg": False, "vqa_xvars": True, "vqa_qwen": False}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_cli_setup_forwards_qwen_flag(monkeypatch):
|
|
25
|
+
captured: dict[str, object] = {}
|
|
26
|
+
|
|
27
|
+
def fake_setup(*, dali: bool, pyg: bool, vqa_xvars: bool, vqa_qwen: bool):
|
|
28
|
+
captured["dali"] = dali
|
|
29
|
+
captured["pyg"] = pyg
|
|
30
|
+
captured["vqa_xvars"] = vqa_xvars
|
|
31
|
+
captured["vqa_qwen"] = vqa_qwen
|
|
32
|
+
|
|
33
|
+
monkeypatch.setattr(cli, "setup", fake_setup)
|
|
34
|
+
|
|
35
|
+
rc = cli.main(["setup", "--vqa_qwen", "--pyg"])
|
|
36
|
+
|
|
37
|
+
assert rc == 0
|
|
38
|
+
assert captured == {"dali": False, "pyg": True, "vqa_xvars": False, "vqa_qwen": True}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_install_xvars_dependencies_uninstalls_then_reinstalls(monkeypatch):
|
|
42
|
+
calls: list[tuple[str, list[str]]] = []
|
|
43
|
+
|
|
44
|
+
def fake_call(cmd: list[str]):
|
|
45
|
+
calls.append(("call", cmd))
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
def fake_check_call(cmd: list[str]):
|
|
49
|
+
calls.append(("check_call", cmd))
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
monkeypatch.setattr(setup_lib.subprocess, "call", fake_call)
|
|
53
|
+
monkeypatch.setattr(setup_lib.subprocess, "check_call", fake_check_call)
|
|
54
|
+
monkeypatch.setattr(setup_lib.sys, "executable", "/usr/bin/python3")
|
|
55
|
+
|
|
56
|
+
setup_lib.install_xvars_dependencies(setup_lib.XVARS_DEPENDENCY_PINS)
|
|
57
|
+
|
|
58
|
+
assert calls == [
|
|
59
|
+
(
|
|
60
|
+
"call",
|
|
61
|
+
[
|
|
62
|
+
"/usr/bin/python3",
|
|
63
|
+
"-m",
|
|
64
|
+
"pip",
|
|
65
|
+
"uninstall",
|
|
66
|
+
"-y",
|
|
67
|
+
"transformers",
|
|
68
|
+
"peft",
|
|
69
|
+
"tokenizers",
|
|
70
|
+
"accelerate",
|
|
71
|
+
"trl",
|
|
72
|
+
],
|
|
73
|
+
),
|
|
74
|
+
(
|
|
75
|
+
"check_call",
|
|
76
|
+
[
|
|
77
|
+
"/usr/bin/python3",
|
|
78
|
+
"-m",
|
|
79
|
+
"pip",
|
|
80
|
+
"install",
|
|
81
|
+
"transformers==4.38.2",
|
|
82
|
+
"peft==0.9.0",
|
|
83
|
+
"tokenizers==0.15.2",
|
|
84
|
+
"accelerate==0.27.2",
|
|
85
|
+
"trl==0.10.1",
|
|
86
|
+
],
|
|
87
|
+
),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_setup_skips_vqa_dependency_install_when_flags_are_false(monkeypatch):
|
|
92
|
+
calls: list[str] = []
|
|
93
|
+
|
|
94
|
+
monkeypatch.setattr(setup_lib, "install_extras", lambda dali=False, pyg=False: calls.append(f"extras:{dali}:{pyg}"))
|
|
95
|
+
monkeypatch.setattr(setup_lib, "install_xvars_dependencies", lambda pins: calls.append(f"deps:{sorted(pins)}"))
|
|
96
|
+
monkeypatch.setattr(setup_lib, "verify", lambda: calls.append("verify"))
|
|
97
|
+
monkeypatch.setattr(setup_lib, "install_torch", lambda: calls.append("torch"))
|
|
98
|
+
|
|
99
|
+
setup_lib.setup(dali=True, pyg=False, vqa_xvars=False, vqa_qwen=False)
|
|
100
|
+
|
|
101
|
+
assert calls == ["torch", "extras:True:False", "verify"]
|
tests/test_vqa_api.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from opensportslib.apis import VQAModel
|
|
10
|
+
from opensportslib.apis._xvars_runtime import (
|
|
11
|
+
XVarsHeadlessRuntime,
|
|
12
|
+
XVarsPrediction,
|
|
13
|
+
_dependency_error,
|
|
14
|
+
build_xvars_prompt,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _FakeRuntime:
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.model = object()
|
|
21
|
+
self.image_processor = object()
|
|
22
|
+
self.calls = []
|
|
23
|
+
|
|
24
|
+
def infer_one(self, *, video_path, question, conv_mode, temperature, max_new_tokens):
|
|
25
|
+
self.calls.append(
|
|
26
|
+
{
|
|
27
|
+
"video_path": video_path,
|
|
28
|
+
"question": question,
|
|
29
|
+
"conv_mode": conv_mode,
|
|
30
|
+
"temperature": temperature,
|
|
31
|
+
"max_new_tokens": max_new_tokens,
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
return XVarsPrediction(
|
|
35
|
+
answer_text=f"answer::{question}",
|
|
36
|
+
action_class="Tackling",
|
|
37
|
+
offence_class="Offence",
|
|
38
|
+
severity_class="3.0",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_vqa_method_signatures_expose_weights(vqa_config_path):
|
|
43
|
+
api = VQAModel(config=vqa_config_path)
|
|
44
|
+
|
|
45
|
+
for method_name in ("load_weights", "train", "infer", "evaluate"):
|
|
46
|
+
sig = inspect.signature(getattr(api, method_name))
|
|
47
|
+
assert "weights" in sig.parameters
|
|
48
|
+
assert any(
|
|
49
|
+
p.kind == inspect.Parameter.VAR_KEYWORD
|
|
50
|
+
for p in sig.parameters.values()
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
infer_sig = inspect.signature(api.infer)
|
|
54
|
+
assert "test_set" in infer_sig.parameters
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_vqa_constructor_weights_are_default_for_infer(vqa_config_path, monkeypatch):
|
|
58
|
+
calls = []
|
|
59
|
+
runtime = _FakeRuntime()
|
|
60
|
+
|
|
61
|
+
def fake_get_runtime(self, weights=None):
|
|
62
|
+
calls.append(weights)
|
|
63
|
+
self.last_loaded_weights = weights
|
|
64
|
+
self.best_checkpoint = weights
|
|
65
|
+
return runtime
|
|
66
|
+
|
|
67
|
+
monkeypatch.setattr(VQAModel, "_get_runtime", fake_get_runtime)
|
|
68
|
+
api = VQAModel(config=vqa_config_path, weights="default-adapter")
|
|
69
|
+
|
|
70
|
+
predictions = api.infer(
|
|
71
|
+
video_path="/tmp/sample.mp4",
|
|
72
|
+
question="Is it a foul or not? Why?",
|
|
73
|
+
use_wandb=False,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assert predictions["task"] == "vqa"
|
|
77
|
+
assert predictions["data"][0]["answer_text"] == "answer::Is it a foul or not? Why?"
|
|
78
|
+
assert calls[-1] == "default-adapter"
|
|
79
|
+
|
|
80
|
+
api.infer(
|
|
81
|
+
video_path="/tmp/sample.mp4",
|
|
82
|
+
question="What card would you give? Why?",
|
|
83
|
+
weights="override-adapter",
|
|
84
|
+
use_wandb=False,
|
|
85
|
+
)
|
|
86
|
+
assert calls[-1] == "override-adapter"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_vqa_single_video_multi_question_infer(vqa_config_path, monkeypatch):
|
|
90
|
+
runtime = _FakeRuntime()
|
|
91
|
+
monkeypatch.setattr(VQAModel, "_get_runtime", lambda self, weights=None: runtime)
|
|
92
|
+
|
|
93
|
+
api = VQAModel(config=vqa_config_path)
|
|
94
|
+
predictions = api.infer(
|
|
95
|
+
video_path="/tmp/clip_0.mp4",
|
|
96
|
+
questions=[
|
|
97
|
+
"Is it a foul or not? Why?",
|
|
98
|
+
"What card would you give? Why?",
|
|
99
|
+
],
|
|
100
|
+
use_wandb=False,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
assert predictions["task"] == "vqa"
|
|
104
|
+
assert [row["id"] for row in predictions["data"]] == ["clip_0:0", "clip_0:1"]
|
|
105
|
+
assert [row["question"] for row in predictions["data"]] == [
|
|
106
|
+
"Is it a foul or not? Why?",
|
|
107
|
+
"What card would you give? Why?",
|
|
108
|
+
]
|
|
109
|
+
assert predictions["data"][0]["video_path"] == "/tmp/clip_0.mp4"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_vqa_dataset_infer_expands_all_questions(vqa_config_path, tmp_path, monkeypatch):
|
|
113
|
+
runtime = _FakeRuntime()
|
|
114
|
+
monkeypatch.setattr(VQAModel, "_get_runtime", lambda self, weights=None: runtime)
|
|
115
|
+
|
|
116
|
+
dataset_path = tmp_path / "test.json"
|
|
117
|
+
dataset_path.write_text(
|
|
118
|
+
json.dumps(
|
|
119
|
+
{
|
|
120
|
+
"data": [
|
|
121
|
+
{
|
|
122
|
+
"id": "action_0",
|
|
123
|
+
"video_path": "/tmp/action_0.mp4",
|
|
124
|
+
"questions": [
|
|
125
|
+
"Is it a foul or not? Why?",
|
|
126
|
+
"What card would you give? Why?",
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": "action_1",
|
|
131
|
+
"inputs": [{"type": "video", "path": "relative/action_1.mp4"}],
|
|
132
|
+
"question": "Could the referee have given advantage? Why?",
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
),
|
|
137
|
+
encoding="utf-8",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
api = VQAModel(config=vqa_config_path)
|
|
141
|
+
predictions = api.infer(test_set=str(dataset_path), use_wandb=False)
|
|
142
|
+
|
|
143
|
+
assert predictions["task"] == "vqa"
|
|
144
|
+
assert len(predictions["data"]) == 3
|
|
145
|
+
assert [row["id"] for row in predictions["data"]] == [
|
|
146
|
+
"action_0:0",
|
|
147
|
+
"action_0:1",
|
|
148
|
+
"action_1",
|
|
149
|
+
]
|
|
150
|
+
assert predictions["data"][2]["video_path"] == str(
|
|
151
|
+
(dataset_path.parent / "relative/action_1.mp4").resolve()
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_build_xvars_prompt_injects_classifier_priors():
|
|
156
|
+
class FakeState:
|
|
157
|
+
def __init__(self):
|
|
158
|
+
self.roles = ("USER", "ASSISTANT")
|
|
159
|
+
self.messages = []
|
|
160
|
+
self.sep = " "
|
|
161
|
+
self.sep2 = "</s>"
|
|
162
|
+
self.sep_style = SimpleNamespace(name="TWO")
|
|
163
|
+
self.action_prompt = ""
|
|
164
|
+
self.offence_prompt = ""
|
|
165
|
+
|
|
166
|
+
def copy(self):
|
|
167
|
+
return FakeState()
|
|
168
|
+
|
|
169
|
+
def append_message(self, role, message):
|
|
170
|
+
self.messages.append([role, message])
|
|
171
|
+
|
|
172
|
+
def set_predictions(self, action_class, offence_class):
|
|
173
|
+
self.action_prompt = action_class
|
|
174
|
+
self.offence_prompt = offence_class
|
|
175
|
+
|
|
176
|
+
def get_prompt(self):
|
|
177
|
+
return (
|
|
178
|
+
f"{self.messages[0][0]}:{self.messages[0][1]}"
|
|
179
|
+
f"|priors:{self.action_prompt}{self.offence_prompt}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
prompt, stop_str = build_xvars_prompt(
|
|
183
|
+
conv_templates={"video-chatgpt_v1": FakeState()},
|
|
184
|
+
conv_mode="video-chatgpt_v1",
|
|
185
|
+
question="What card would you give? Why?",
|
|
186
|
+
replace_token="<vid_tokens>",
|
|
187
|
+
video_token="<video>",
|
|
188
|
+
action_prompt="a tackle",
|
|
189
|
+
offence_prompt=", foul and a yellow card",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
assert "<vid_tokens>" in prompt
|
|
193
|
+
assert "a tackle, foul and a yellow card" in prompt
|
|
194
|
+
assert "<video>" not in prompt
|
|
195
|
+
assert stop_str == "</s>"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_vqa_save_predictions_round_trip(vqa_config_path, tmp_path, monkeypatch):
|
|
199
|
+
runtime = _FakeRuntime()
|
|
200
|
+
monkeypatch.setattr(VQAModel, "_get_runtime", lambda self, weights=None: runtime)
|
|
201
|
+
|
|
202
|
+
api = VQAModel(config=vqa_config_path)
|
|
203
|
+
predictions = api.infer(
|
|
204
|
+
video_path="/tmp/clip_0.mp4",
|
|
205
|
+
question="Is it a foul or not? Why?",
|
|
206
|
+
use_wandb=False,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
out_path = tmp_path / "predictions.json"
|
|
210
|
+
saved = api.save_predictions(str(out_path), predictions)
|
|
211
|
+
|
|
212
|
+
assert saved == str(out_path)
|
|
213
|
+
assert json.loads(out_path.read_text(encoding="utf-8"))["task"] == "vqa"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_vqa_train_and_evaluate_raise_not_implemented(vqa_config_path):
|
|
217
|
+
api = VQAModel(config=vqa_config_path)
|
|
218
|
+
|
|
219
|
+
with pytest.raises(NotImplementedError):
|
|
220
|
+
api.train(use_wandb=False)
|
|
221
|
+
|
|
222
|
+
with pytest.raises(NotImplementedError):
|
|
223
|
+
api.evaluate(use_wandb=False)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_vqa_import_does_not_pull_gradio_or_demo_modules():
|
|
227
|
+
assert "gradio" not in sys.modules
|
|
228
|
+
assert "video_chatgpt.demo.chat" not in sys.modules
|
|
229
|
+
assert "video_chatgpt.demo" not in sys.modules
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_vqa_runtime_dependency_error_names_missing_package(monkeypatch):
|
|
233
|
+
runtime = XVarsHeadlessRuntime()
|
|
234
|
+
|
|
235
|
+
def fail_bootstrap():
|
|
236
|
+
raise ModuleNotFoundError("No module named 'numpy'", name="numpy")
|
|
237
|
+
|
|
238
|
+
monkeypatch.setattr(runtime, "_bootstrap_components", fail_bootstrap)
|
|
239
|
+
error = _dependency_error(ModuleNotFoundError("No module named 'numpy'", name="numpy"))
|
|
240
|
+
assert "numpy" in str(error)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
|
|
5
|
+
from opensportslib.models.base.qwen_xvars import QwenXVarsCausalLM, QwenXVarsModel
|
|
6
|
+
from opensportslib.models.builder import build_model
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TinyTokenizer:
|
|
10
|
+
pad_token_id = 0
|
|
11
|
+
eos_token_id = 1
|
|
12
|
+
pad_token = "<pad>"
|
|
13
|
+
eos_token = "</s>"
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.vocab = {"<pad>": 0, "</s>": 1, "<vid_patch>": 2, "<vid_start>": 3, "<vid_end>": 4}
|
|
17
|
+
|
|
18
|
+
def get_vocab(self):
|
|
19
|
+
return dict(self.vocab)
|
|
20
|
+
|
|
21
|
+
def add_special_tokens(self, payload):
|
|
22
|
+
added = 0
|
|
23
|
+
for token in payload.get("additional_special_tokens", []):
|
|
24
|
+
if token not in self.vocab:
|
|
25
|
+
self.vocab[token] = len(self.vocab)
|
|
26
|
+
added += 1
|
|
27
|
+
return added
|
|
28
|
+
|
|
29
|
+
def convert_tokens_to_ids(self, token):
|
|
30
|
+
return self.vocab[token]
|
|
31
|
+
|
|
32
|
+
def __len__(self):
|
|
33
|
+
return len(self.vocab)
|
|
34
|
+
|
|
35
|
+
def __call__(self, text, return_tensors=None, **kwargs):
|
|
36
|
+
del kwargs
|
|
37
|
+
if isinstance(text, list):
|
|
38
|
+
text = text[0]
|
|
39
|
+
tokens = []
|
|
40
|
+
i = 0
|
|
41
|
+
while i < len(text):
|
|
42
|
+
matched = False
|
|
43
|
+
for token in ("<vid_start>", "<vid_patch>", "<vid_end>", "</s>"):
|
|
44
|
+
if text.startswith(token, i):
|
|
45
|
+
tokens.append(token)
|
|
46
|
+
i += len(token)
|
|
47
|
+
matched = True
|
|
48
|
+
break
|
|
49
|
+
if matched:
|
|
50
|
+
continue
|
|
51
|
+
if text[i].isspace():
|
|
52
|
+
i += 1
|
|
53
|
+
continue
|
|
54
|
+
j = i
|
|
55
|
+
while j < len(text) and not text[j].isspace() and not any(
|
|
56
|
+
text.startswith(token, j) for token in ("<vid_start>", "<vid_patch>", "<vid_end>", "</s>")
|
|
57
|
+
):
|
|
58
|
+
j += 1
|
|
59
|
+
tokens.append(text[i:j])
|
|
60
|
+
i = j
|
|
61
|
+
ids = []
|
|
62
|
+
for token in tokens:
|
|
63
|
+
if token not in self.vocab:
|
|
64
|
+
self.vocab[token] = len(self.vocab)
|
|
65
|
+
ids.append(self.vocab[token])
|
|
66
|
+
attn = [1] * len(ids)
|
|
67
|
+
if return_tensors == "pt":
|
|
68
|
+
return {"input_ids": torch.tensor([ids]), "attention_mask": torch.tensor([attn])}
|
|
69
|
+
return SimpleNamespace(input_ids=ids, attention_mask=attn)
|
|
70
|
+
|
|
71
|
+
def batch_decode(self, sequences, skip_special_tokens=True):
|
|
72
|
+
del sequences, skip_special_tokens
|
|
73
|
+
return ["qwen answer </s>"]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TinyGenerateLM(torch.nn.Module):
|
|
77
|
+
def __init__(self):
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.config = SimpleNamespace(hidden_size=4, vocab_size=64, use_cache=True)
|
|
80
|
+
self.generation_config = SimpleNamespace(use_cache=False)
|
|
81
|
+
self.emb = torch.nn.Embedding(128, 4)
|
|
82
|
+
self.lm = torch.nn.Linear(4, 64)
|
|
83
|
+
self.seen_inputs_embeds = None
|
|
84
|
+
self.generate_kwargs = None
|
|
85
|
+
|
|
86
|
+
def get_input_embeddings(self):
|
|
87
|
+
return self.emb
|
|
88
|
+
|
|
89
|
+
def resize_token_embeddings(self, size):
|
|
90
|
+
return self.emb
|
|
91
|
+
|
|
92
|
+
def forward(self, input_ids=None, inputs_embeds=None, attention_mask=None, labels=None, **kwargs):
|
|
93
|
+
del input_ids, attention_mask, kwargs
|
|
94
|
+
self.seen_inputs_embeds = inputs_embeds.detach().clone()
|
|
95
|
+
logits = self.lm(inputs_embeds)
|
|
96
|
+
loss = logits.sum() * 0
|
|
97
|
+
if labels is not None:
|
|
98
|
+
loss = loss + 0.123
|
|
99
|
+
return SimpleNamespace(loss=loss, logits=logits)
|
|
100
|
+
|
|
101
|
+
def generate(self, input_ids=None, inputs_embeds=None, attention_mask=None, **kwargs):
|
|
102
|
+
del attention_mask
|
|
103
|
+
self.generate_kwargs = {"input_ids": input_ids, "inputs_embeds": inputs_embeds, **kwargs}
|
|
104
|
+
extra = torch.tensor([[9, 1]], device=input_ids.device if input_ids is not None else inputs_embeds.device)
|
|
105
|
+
if input_ids is None:
|
|
106
|
+
prefix = torch.zeros((1, inputs_embeds.shape[1]), dtype=torch.long, device=extra.device)
|
|
107
|
+
return torch.cat((prefix, extra), dim=1)
|
|
108
|
+
return torch.cat((input_ids, extra), dim=1)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _cfg(tmp_path):
|
|
112
|
+
return SimpleNamespace(
|
|
113
|
+
TASK="VQA",
|
|
114
|
+
SYSTEM=SimpleNamespace(device="cpu", gpu=SimpleNamespace(count=0, id=0)),
|
|
115
|
+
MODEL=SimpleNamespace(
|
|
116
|
+
runtime=SimpleNamespace(dtype="fp32"),
|
|
117
|
+
load=SimpleNamespace(checkpoint_path=None),
|
|
118
|
+
components=SimpleNamespace(
|
|
119
|
+
video_encoder=SimpleNamespace(
|
|
120
|
+
kind="encoder",
|
|
121
|
+
source=SimpleNamespace(provider="opensportslib", name="xvars_clip_features"),
|
|
122
|
+
load=SimpleNamespace(weights_path=str(tmp_path / "vision.pt")),
|
|
123
|
+
params=SimpleNamespace(feature_source="indexed_or_raw_clip", vision_tower="clip", feature_dim=1024),
|
|
124
|
+
overrides=SimpleNamespace(),
|
|
125
|
+
),
|
|
126
|
+
mm_projector=SimpleNamespace(
|
|
127
|
+
kind="projector",
|
|
128
|
+
source=SimpleNamespace(provider="opensportslib"),
|
|
129
|
+
params=SimpleNamespace(input_dim=1024),
|
|
130
|
+
overrides=SimpleNamespace(),
|
|
131
|
+
),
|
|
132
|
+
llm_decoder=SimpleNamespace(
|
|
133
|
+
kind="decoder",
|
|
134
|
+
source=SimpleNamespace(provider="huggingface", name="Qwen/Qwen3.5-9B-Base"),
|
|
135
|
+
params=SimpleNamespace(repo_id="Qwen/Qwen3.5-9B-Base"),
|
|
136
|
+
overrides=SimpleNamespace(),
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
metadata=SimpleNamespace(backend="qwen_xvars_infer"),
|
|
140
|
+
),
|
|
141
|
+
DATA=SimpleNamespace(
|
|
142
|
+
inputs=SimpleNamespace(video=SimpleNamespace(sampling=SimpleNamespace(num_frames=100, start_frame=63, end_frame=87, input_fps=25, target_fps=17))),
|
|
143
|
+
),
|
|
144
|
+
TRAIN=SimpleNamespace(
|
|
145
|
+
execution={
|
|
146
|
+
"prompt": {"include_priors": True, "video_token_len": 3},
|
|
147
|
+
"generation": {"max_new_tokens": 8, "temperature": 0.0},
|
|
148
|
+
"xvars": {"feature_mode": "strict_xvars"},
|
|
149
|
+
"hf": {"local_files_only": True, "prefer_cuda": False, "tokenizer_id": "Qwen/Qwen3.5-9B-Base"},
|
|
150
|
+
"quantization": {"enabled": False},
|
|
151
|
+
}
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_qwen_xvars_wrapper_projects_features_into_hidden_size():
|
|
157
|
+
tokenizer = TinyTokenizer()
|
|
158
|
+
base = TinyGenerateLM()
|
|
159
|
+
model = QwenXVarsCausalLM(base, mm_hidden_size=1024)
|
|
160
|
+
input_ids = torch.tensor([[tokenizer.convert_tokens_to_ids("<vid_start>"), tokenizer.convert_tokens_to_ids("<vid_patch>"), tokenizer.convert_tokens_to_ids("<vid_patch>"), tokenizer.convert_tokens_to_ids("<vid_end>")]])
|
|
161
|
+
features = torch.ones((1, 2, 1024))
|
|
162
|
+
|
|
163
|
+
out = model(
|
|
164
|
+
input_ids=input_ids,
|
|
165
|
+
attention_mask=torch.ones_like(input_ids),
|
|
166
|
+
labels=input_ids.clone(),
|
|
167
|
+
video_spatio_temporal_features=features,
|
|
168
|
+
tokenizer=tokenizer,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
assert abs(out.loss.item() - 0.123) < 1e-6
|
|
172
|
+
assert model.mm_projector.out_features == base.config.hidden_size
|
|
173
|
+
assert tuple(base.seen_inputs_embeds.shape) == (1, 4, base.config.hidden_size)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_qwen_xvars_model_generate_answer_uses_existing_feature_contract(tmp_path, monkeypatch):
|
|
177
|
+
cfg = _cfg(tmp_path)
|
|
178
|
+
tokenizer = TinyTokenizer()
|
|
179
|
+
base_model = TinyGenerateLM()
|
|
180
|
+
|
|
181
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
182
|
+
|
|
183
|
+
monkeypatch.setattr(AutoTokenizer, "from_pretrained", lambda *args, **kwargs: tokenizer)
|
|
184
|
+
monkeypatch.setattr(AutoModelForCausalLM, "from_pretrained", lambda *args, **kwargs: base_model)
|
|
185
|
+
|
|
186
|
+
model = QwenXVarsModel(cfg, model_id="Qwen/Qwen3.5-9B-Base", projector_params={"input_dim": 1024})
|
|
187
|
+
answer = model.generate_answer(
|
|
188
|
+
{
|
|
189
|
+
"id": "action_0",
|
|
190
|
+
"question": "Was this a foul?",
|
|
191
|
+
"references": [],
|
|
192
|
+
"labels": {},
|
|
193
|
+
"metadata": {},
|
|
194
|
+
"prior_prediction_text": "",
|
|
195
|
+
"video_spatio_temporal_features": torch.ones((3, 1024)),
|
|
196
|
+
},
|
|
197
|
+
prompt_cfg={"video_token_len": 3},
|
|
198
|
+
generation_cfg={"max_new_tokens": 8, "temperature": 0.0},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
assert model._ready is True
|
|
202
|
+
assert answer == "qwen answer"
|
|
203
|
+
assert base_model.generate_kwargs is not None
|
|
204
|
+
assert base_model.generate_kwargs["inputs_embeds"].shape[-1] == base_model.config.hidden_size
|
|
205
|
+
assert base_model.generate_kwargs["inputs_embeds"].shape[-2] == base_model.generate_kwargs["input_ids"].shape[-1]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_qwen_loader_does_not_pass_use_cache_to_from_pretrained(tmp_path, monkeypatch):
|
|
209
|
+
cfg = _cfg(tmp_path)
|
|
210
|
+
tokenizer = TinyTokenizer()
|
|
211
|
+
base_model = TinyGenerateLM()
|
|
212
|
+
captured = {}
|
|
213
|
+
|
|
214
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
215
|
+
|
|
216
|
+
def _load_tokenizer(*args, **kwargs):
|
|
217
|
+
del args, kwargs
|
|
218
|
+
return tokenizer
|
|
219
|
+
|
|
220
|
+
def _load_model(model_id, **kwargs):
|
|
221
|
+
captured["model_id"] = model_id
|
|
222
|
+
captured["model_kwargs"] = dict(kwargs)
|
|
223
|
+
if "use_cache" in kwargs:
|
|
224
|
+
raise TypeError("unexpected load kwarg")
|
|
225
|
+
return base_model
|
|
226
|
+
|
|
227
|
+
monkeypatch.setattr(AutoTokenizer, "from_pretrained", _load_tokenizer)
|
|
228
|
+
monkeypatch.setattr(AutoModelForCausalLM, "from_pretrained", _load_model)
|
|
229
|
+
|
|
230
|
+
model = QwenXVarsModel(cfg, model_id="Qwen/Qwen3.5-9B-Base", projector_params={"input_dim": 1024})
|
|
231
|
+
|
|
232
|
+
assert model._ready is True
|
|
233
|
+
assert captured["model_id"] == "Qwen/Qwen3.5-9B-Base"
|
|
234
|
+
assert "use_cache" not in captured["model_kwargs"]
|
|
235
|
+
assert base_model.config.use_cache is True
|
|
236
|
+
assert base_model.generation_config.use_cache is True
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_builder_routes_qwen_backend(tmp_path, monkeypatch):
|
|
240
|
+
cfg = _cfg(tmp_path)
|
|
241
|
+
tokenizer = TinyTokenizer()
|
|
242
|
+
base_model = TinyGenerateLM()
|
|
243
|
+
|
|
244
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
245
|
+
|
|
246
|
+
monkeypatch.setattr(AutoTokenizer, "from_pretrained", lambda *args, **kwargs: tokenizer)
|
|
247
|
+
monkeypatch.setattr(AutoModelForCausalLM, "from_pretrained", lambda *args, **kwargs: base_model)
|
|
248
|
+
|
|
249
|
+
model, _ = build_model(cfg, device="cpu")
|
|
250
|
+
|
|
251
|
+
assert isinstance(model, QwenXVarsModel)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|