bpiyush commited on
Commit
7daf628
·
1 Parent(s): 1bbdaa5

Update TARA to latest Tarsier2 checkpoint and runnable demo.

Browse files

Replace weights and tokenizer/config artifacts, refresh README/demo/runtime code for the new stack, and ensure large files (including tokenizer.json) are tracked via LFS for Hub compatibility.

Made-with: Cursor

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. README.md +178 -71
  3. added_tokens.json +14 -1
  4. chat_template.json +3 -0
  5. config.json +214 -44
  6. demo_usage.py +25 -28
  7. generation_config.json +3 -5
  8. merges.txt +0 -0
  9. model-00001-of-00003.safetensors → model-00001-of-00004.safetensors +2 -2
  10. model-00002-of-00003.safetensors → model-00002-of-00004.safetensors +2 -2
  11. model-00003-of-00003.safetensors → model-00003-of-00004.safetensors +2 -2
  12. tokenizer.model → model-00004-of-00004.safetensors +2 -2
  13. model.safetensors.index.json +0 -0
  14. modeling_tara.py +288 -223
  15. preprocessor_config.json +10 -9
  16. processor_config.json +6 -4
  17. shared/__init__.py +0 -0
  18. shared/run/cut_clips_ego4d.sh +35 -0
  19. shared/run/extract_feat_dinov2_ego4d.sh +12 -0
  20. shared/run/extract_feat_pe_ego4d.sh +17 -0
  21. shared/run/extract_feat_pe_ego4d_reverse.sh +16 -0
  22. shared/run/generate_water.py +24 -0
  23. shared/run/generate_water_v2.py +41 -0
  24. shared/run/run.sh +4 -0
  25. shared/scripts/avi_to_mp4.py +50 -0
  26. shared/scripts/check_cut_files.py +50 -0
  27. shared/scripts/check_video_health.py +109 -0
  28. shared/scripts/check_webdataset.py +69 -0
  29. shared/scripts/convert_frames_to_videos.py +70 -0
  30. shared/scripts/convert_webm_to_mp4.py +44 -0
  31. shared/scripts/create_webdataset.py +265 -0
  32. shared/scripts/cut_clips.py +203 -0
  33. shared/scripts/cut_clips_fast.py +200 -0
  34. shared/scripts/cut_multiple_clips.py +357 -0
  35. shared/scripts/downscale_videos.py +274 -0
  36. shared/scripts/downsize_videos.py +194 -0
  37. shared/scripts/downsize_videos_simple.py +150 -0
  38. shared/scripts/extract_speed_clips.py +126 -0
  39. shared/scripts/save_grid_of_videos.py +203 -0
  40. shared/scripts/shard_video_dataset.py +71 -0
  41. shared/utils/__init__.py +16 -0
  42. shared/utils/audio.py +227 -0
  43. shared/utils/av.py +93 -0
  44. shared/utils/classification.py +47 -0
  45. shared/utils/epic.py +15 -0
  46. shared/utils/gif.py +609 -0
  47. shared/utils/hardware.py +68 -0
  48. shared/utils/image.py +81 -0
  49. shared/utils/io.py +194 -0
  50. shared/utils/keypoint_matching.py +330 -0
.gitattributes CHANGED
@@ -6,6 +6,7 @@
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
 
9
  *.joblib filter=lfs diff=lfs merge=lfs -text
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
  *.mlmodel filter=lfs diff=lfs merge=lfs -text
@@ -33,6 +34,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
36
  assets/cat.png filter=lfs diff=lfs merge=lfs -text
37
  assets/dog+cat.png filter=lfs diff=lfs merge=lfs -text
38
  assets/source-27375787.mp4 filter=lfs diff=lfs merge=lfs -text
 
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.jar filter=lfs diff=lfs merge=lfs -text
10
  *.joblib filter=lfs diff=lfs merge=lfs -text
11
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
12
  *.mlmodel filter=lfs diff=lfs merge=lfs -text
 
34
  *.zip filter=lfs diff=lfs merge=lfs -text
35
  *.zst filter=lfs diff=lfs merge=lfs -text
36
  *tfevents* filter=lfs diff=lfs merge=lfs -text
37
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
38
  assets/cat.png filter=lfs diff=lfs merge=lfs -text
39
  assets/dog+cat.png filter=lfs diff=lfs merge=lfs -text
40
  assets/source-27375787.mp4 filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,27 +1,56 @@
1
- ---
2
- license: apache-2.0
3
- datasets:
4
- - sentence-transformers/all-nli
5
- language:
6
- - en
7
- metrics:
8
- - accuracy
9
- base_model:
10
- - omni-research/Tarsier-7b
11
- tags:
12
- - video-retrieval
13
- - text-to-video-retrieval
14
- - time-awareness
15
- - video-models
16
- ---
17
-
18
- # ![](assets/tara-logo.png) TARA: Time-Aware Retrieval Adaptation for Video Understanding
19
  <!-- # <img src="./assets/logo.png" width="24"> TARA: Time-Aware Retrieval Adaptation for Video Understanding -->
20
 
21
- TARA (Time-Aware Retrieval Adaptation) is a multimodal model for video and text understanding.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  ## Installation & Setup
24
 
 
 
 
 
 
 
 
25
  ### 1. Install Git LFS (if not already installed)
26
 
27
  Git LFS is required to download the model weights.
@@ -43,9 +72,9 @@ Git LFS initialized.
43
  ```
44
 
45
 
46
- ### 2. Clone the Repository
47
  ```bash
48
- git clone https://huggingface.co/bpiyush/TARA
49
  cd TARA
50
  ```
51
 
@@ -76,10 +105,37 @@ This will download all model weights (may take a few minutes depending on your c
76
 
77
  ## Quick Start
78
 
79
- See the script at [demo_usage.py](demo_usage.py) for a quick start. You can run it:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
  ```sh
82
- python demo_usage.py
83
  ```
84
  The output should look something like this:
85
 
@@ -88,45 +144,50 @@ The output should look something like this:
88
  TARA Model Demo
89
  ============================================================
90
 
91
- [1/6] Loading model...
92
- [ MODEL ] Loading TARA from /work/piyush/pretrained_checkpoints/TARA/ [..............]
93
- ### do_image_padding is set as False, images will be resized directly!
 
94
  The model weights are not tied. Please use the `tie_weights` method before using the `infer_auto_device` function.
95
- Loading checkpoint shards: 100%|██████████████████████████████████████████████████████████████████████████████████████| 3/3 [00:03<00:00, 1.05s/it]
96
  ✓ Model loaded successfully!
97
- Number of parameters: 7.063B
98
  ----------------------------------------------------------------------------------------------------
99
 
100
- [2/6] Testing video encoding and captioning ...
 
101
  ✓ Video encoded successfully!
102
- Video shape: torch.Size([1, 16, 3, 240, 426])
103
- Video embedding shape: torch.Size([4096])
104
- Video caption: A hand is seen folding a white paper on a gray carpeted floor. The paper is opened flat on the surface, and then the hand folds it in half vertically, creating a crease in the middle. The hand continues to fold the paper further, resulting in a smaller, more compact size. The background remains a consistent gray carpet throughout the video.
105
  ----------------------------------------------------------------------------------------------------
106
 
107
- [3/6] Testing text encoding...
 
 
 
108
  ✓ Text encoded successfully!
109
  Text: ['someone is folding a paper', 'cutting a paper', 'someone is unfolding a paper']
110
- Text embedding shape: torch.Size([3, 4096])
111
 
112
- [4/6] Computing video-text similarities...
113
  ✓ Similarities computed!
114
- 'someone is folding a paper': 0.5039
115
- 'cutting a paper': 0.3022
116
- 'someone is unfolding a paper': 0.3877
117
  ----------------------------------------------------------------------------------------------------
118
 
119
- [5/6] Testing negation example...
120
- Image embedding shape: torch.Size([2, 4096])
 
121
  Text query: ['an image of a cat but there is no dog in it']
122
- Text-Image similarity: tensor([[0.2585, 0.1449]])
123
  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
124
  Text query: ['an image of a cat and a dog together']
125
- Text-Image similarity: tensor([[0.2815, 0.4399]])
126
  ----------------------------------------------------------------------------------------------------
127
 
128
- [6/6] Testing composed video retrieval...
129
- Source-Target similarity with edit: 0.6476313471794128
130
 
131
  ============================================================
132
  Demo completed successfully! 🎉
@@ -134,45 +195,91 @@ Demo completed successfully! 🎉
134
  ```
135
 
136
 
137
- OR use the snippet below:
138
 
139
- ```python
140
- import torch
141
- from modeling_tara import TARA, read_frames_decord
142
 
143
- model = TARA.from_pretrained(
144
- ".", # Load from current directory
145
- device_map='auto',
146
- torch_dtype=torch.bfloat16,
147
- )
148
- n_params = sum(p.numel() for p in model.model.parameters())
149
- print(f"Number of parameters: {round(n_params/1e9, 3)}B")
150
 
151
- # Embed a video
152
- video_path = "./assets/folding_paper.mp4"
153
- video_tensor = read_frames_decord(video_path, num_frames=16)
154
- video_tensor = video_tensor.unsqueeze(0)
155
- video_tensor = video_tensor.to(model.model.device)
156
- with torch.no_grad():
157
- video_emb = model.encode_vision(video_tensor).cpu().squeeze(0).float()
158
- print(f"Video shape: {video_tensor.shape}") # torch.Size([1, 16, 3, 240, 426])
159
- print(f"Video embedding shape: {video_emb.shape}") # torch.Size([4096])
160
 
161
- # Embed a text
162
- text = ['someone is folding a paper', 'cutting a paper', 'someone is folding a paper']
163
- with torch.no_grad():
164
- text_emb = model.encode_text(text).cpu().float()
165
- print(f"Text embedding shape: {text_emb.shape}") # torch.Size([3, 4096])
 
166
  ```
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  ## Citation
169
 
170
  If you use this model, please cite:
171
  ```bibtex
172
- @misc{tara2025,
173
- title={TARA: Simple and Efficient Time Aware Retrieval Adaptation of MLLMs for Video Understanding},
174
  author={Piyush Bagad and Andrew Zisserman},
175
  year={2025}
 
 
 
 
 
 
 
 
 
 
176
  }
177
  ```
178
 
 
1
+ # ![](assets/tara-logo.png) TARA: *T*ext *A*dapted *R*etrieval *A*lignment for Nuanced Video Retrieval
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  <!-- # <img src="./assets/logo.png" width="24"> TARA: Time-Aware Retrieval Adaptation for Video Understanding -->
3
 
4
+ This repository contains inference and evaluation code for the TARA model based on the paper:
5
+ [Adapting MLLMs for Nuanced Video Retrieval](https://arxiv.org/abs/2512.13511)
6
+
7
+ <p align="center">
8
+ <a href="https://bpiyush.github.io/tara-website/" target="_blank">
9
+ <img src="https://img.shields.io/badge/Project-Page-blue" alt="Project Page">
10
+ </a>
11
+ &nbsp;&nbsp;&nbsp;
12
+ <a href="https://github.com/bpiyush/TARA" target="_blank">
13
+ <img src="https://img.shields.io/badge/GitHub-Code-black?logo=github" alt="GitHub Code">
14
+ </a>
15
+ &nbsp;&nbsp;&nbsp;
16
+ <a href="https://arxiv.org/abs/2512.13511" target="_blank">
17
+ <img src="https://img.shields.io/badge/arXiv-Paper-b31b1b?logo=arxiv&logoColor=white" alt="arXiv">
18
+ </a>
19
+ &nbsp;&nbsp;&nbsp;
20
+ <a href="https://huggingface.co/datasets/bpiyush/chirality-in-action" target="_blank">
21
+ <img src="https://huggingface.co/datasets/huggingface/badges/resolve/main/dataset-on-hf-md-dark.svg" alt="Dataset on Hugging Face">
22
+ </a>
23
+ </p>
24
+
25
+ <!-- Show arch fig in 75% of the screen and center it -->
26
+ <p align="center">
27
+ <img src="./assets/arch.png" width="75%" alt="TARA architecture">
28
+ </p>
29
+ <!-- Add a caption with small font size and center it such that it align with image width and center it-->
30
+ <p style="text-align: left; font-size: 13px; width: 75%; display: block; margin: 0 auto;"><b>TARA Architecture:</b> We use EOL prompt to embed videos using an MLLM (Tarsier2-7B). We train the LLM weights with contrastive loss on carefully crafted hard-negatives to instill (i) temporal, (ii) negation and (iii) multimodal nuances in the embedding space.</p>
31
+
32
+
33
+ <!-- Add a Table of Contents here -->
34
+ ## Table of Contents
35
+ - [Installation & Setup](#installation--setup)
36
+ - [Quick Start](#quick-start)
37
+ - [Evaluation](#evaluation)
38
+ - [Data Preparation](#data-preparation)
39
+ - [Embedding Computation](#embedding-computation)
40
+ - [General evaluation: MMEB-V2 (Meng et al.)](#general-evaluation-mmeb-v2-meng-et-al)
41
+ - [Citation](#citation)
42
+ - [License](#license)
43
+
44
 
45
  ## Installation & Setup
46
 
47
+ First, clone the repository:
48
+ ```bash
49
+ git clone https://github.com/bpiyush/tara.git
50
+ cd tara
51
+ ```
52
+
53
+
54
  ### 1. Install Git LFS (if not already installed)
55
 
56
  Git LFS is required to download the model weights.
 
72
  ```
73
 
74
 
75
+ ### 2. Download the Model Weights
76
  ```bash
77
+ git clone https://huggingface.co/bpiyush/TARA /path/to/download/tara
78
  cd TARA
79
  ```
80
 
 
105
 
106
  ## Quick Start
107
 
108
+ TARA is primarily designed to encode videos and texts in a joint embedding space under an MLLM.
109
+
110
+ ```python
111
+ import torch
112
+ from modeling_tara import TARA
113
+
114
+ model = TARA.from_pretrained(
115
+ "/path/to/download/tara", # Load from current directory
116
+ device_map='auto',
117
+ torch_dtype=torch.bfloat16,
118
+ )
119
+ n_params = sum(p.numel() for p in model.model.parameters())
120
+ print(f"Number of parameters: {round(n_params/1e9, 3)}B")
121
+
122
+ # Embed a video
123
+ video_path = "./assets/folding_paper.mp4"
124
+ with torch.no_grad():
125
+ video_emb = model.encode_vision(video_path).cpu().squeeze(0).float()
126
+ print(f"Video embedding shape: {video_emb.shape}") # torch.Size([3584])
127
+
128
+ # Embed a text
129
+ text = ['someone is folding a paper', 'cutting a paper', 'someone is folding a paper']
130
+ with torch.no_grad():
131
+ text_emb = model.encode_text(text).cpu().float()
132
+ print(f"Text embedding shape: {text_emb.shape}") # torch.Size([3, 3584])
133
+ ```
134
+
135
+ For a more detailed demo, see the script at [demo_usage.py](demo_usage.py). You can run it:
136
 
137
  ```sh
138
+ python demo_usage.py --model_path /path/to/download/tara
139
  ```
140
  The output should look something like this:
141
 
 
144
  TARA Model Demo
145
  ============================================================
146
 
147
+ [1/5] Loading model...
148
+ The argument `trust_remote_code` is to be used with Auto classes. It has no effect here and is ignored.
149
+ Unrecognized keys in `rope_scaling` for 'rope_type'='default': {'mrope_section'}
150
+ The argument `trust_remote_code` is to be used with Auto classes. It has no effect here and is ignored.
151
  The model weights are not tied. Please use the `tie_weights` method before using the `infer_auto_device` function.
152
+ Loading checkpoint shards: 100%|████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [00:03<00:00, 1.07it/s]
153
  ✓ Model loaded successfully!
154
+ Number of parameters: 8.291B
155
  ----------------------------------------------------------------------------------------------------
156
 
157
+ [2/5] Testing video encoding ...
158
+ From v4.47 onwards, when a model cache is to be returned, `generate` will return a `Cache` instance instead by default (as opposed to the legacy tuple of tuples format). If you want to keep returning the legacy format, please set `return_legacy_cache=True`.
159
  ✓ Video encoded successfully!
160
+ Video embedding shape: torch.Size([3584])
 
 
161
  ----------------------------------------------------------------------------------------------------
162
 
163
+ [3/5] Testing text encoding...
164
+ Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
165
+ Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
166
+ Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
167
  ✓ Text encoded successfully!
168
  Text: ['someone is folding a paper', 'cutting a paper', 'someone is unfolding a paper']
169
+ Text embedding shape: torch.Size([3, 3584])
170
 
171
+ [4/5] Computing video-text similarities...
172
  ✓ Similarities computed!
173
+ 'someone is folding a paper': 0.6488
174
+ 'cutting a paper': 0.3952
175
+ 'someone is unfolding a paper': 0.3009
176
  ----------------------------------------------------------------------------------------------------
177
 
178
+ [5/5] Testing negation example...
179
+ Image embedding shape: torch.Size([2, 3584])
180
+ Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
181
  Text query: ['an image of a cat but there is no dog in it']
182
+ Text-Image similarity: tensor([[0.5169, 0.3659]])
183
  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
184
+ Setting `pad_token_id` to `eos_token_id`:None for open-end generation.
185
  Text query: ['an image of a cat and a dog together']
186
+ Text-Image similarity: tensor([[0.4364, 0.6004]])
187
  ----------------------------------------------------------------------------------------------------
188
 
189
+ [Bonus] Testing composed video retrieval...
190
+ Source-Target similarity with edit: 0.757888674736023
191
 
192
  ============================================================
193
  Demo completed successfully! 🎉
 
195
  ```
196
 
197
 
198
+ ## Evaluation
199
 
 
 
 
200
 
201
+ ### Data Preparation
 
 
 
 
 
 
202
 
203
+ We release the nuanced video retrieval splits used in the dataset in [data/](data/) folder.
204
+ For ease of use, we have combined all the data for (i) temporal, (ii) negation and (iii) multimodal
205
+ nuance into a single file where each entry is a video/text/video-text/image, etc.
 
 
 
 
 
 
206
 
207
+ ```sh
208
+ data
209
+ ├── nuanced_retrieval_inputs-test.csv # List of examples to embed (video, text, composed video-text, etc.) for test set
210
+ ├── nuanced_retrieval_inputs-val.csv # List of examples to embed (video, text, composed video-text, etc.) for validation set
211
+ ├── nuanced_retrieval_labels-test.json # Labels for test set
212
+ └── nuanced_retrieval_labels-val.json # Labels for validation set
213
  ```
214
 
215
+ An example input row looks like this:
216
+ ```json
217
+ {
218
+ 'id': '138629',
219
+ 'value': '138629',
220
+ 'nuance': 'time',
221
+ 'source': 'cia-ssv2',
222
+ 'modality': 'video',
223
+ }
224
+ ```
225
+ where `id`is the unique identified, `value` is actual value (e.g., for a text caption, the ID can be different and value stores the actual caption), `nuance` is the type of nuance,
226
+ `source` is the source of the example (e.g., `cia-ssv2` for SSv2), and `modality` is the modality of the example (e.g., `video` or `text`).
227
+
228
+
229
+ The coresponding label looks like this:
230
+ ```json
231
+ ['12055391_1.0']
232
+ ```
233
+ which denotes the `id` of the text associated with the video.
234
+
235
+ Finally, set the right paths to the data directories in [evals/compute_embeddings.py](evals/compute_embeddings.py)
236
+ based on your local setup.
237
+
238
+ ### Embedding Computation
239
+
240
+ First, you need to compute the embeddings for the entire dataset. You can do this by running the following script:
241
+
242
+ ```bash
243
+ python evals/compute_embeddings.py \
244
+ --model_path /path/to/download/tara \
245
+ --csv_path ./data/nuanced_retrieval_inputs-val.csv \
246
+ --model_name tara_7b
247
+ ```
248
+
249
+ Then, run the script to compute retrieval metrics.
250
+
251
+ ```bash
252
+ python evals/compute_metrics.py \
253
+ --model_path /path/to/download/tara \
254
+ --csv_path ./data/nuanced_retrieval_inputs-val.csv \
255
+ --lab_path ./data/nuanced_retrieval_labels-val.json \
256
+ --model_name tara_7b
257
+ ```
258
+
259
+ ### General evaluation: MMEB-V2 ([Meng et al.](https://arxiv.org/abs/2507.04590))
260
+
261
+ We evaluate on the video classification and video retrieval tasks in MMEB-V2 to demonstrate the generalizability of TARA.
262
+
263
+ TODO
264
+
265
  ## Citation
266
 
267
  If you use this model, please cite:
268
  ```bibtex
269
+ @article{tara2025,
270
+ title={Adapting MLLMs for Nuanced Video Retrieval},
271
  author={Piyush Bagad and Andrew Zisserman},
272
  year={2025}
273
+ journal={arXiv preprint arXiv:2512.13511}
274
+ }
275
+ ```
276
+
277
+ ```bibtex
278
+ @article{bagad2025chirality,
279
+ title={Chirality in Action: Time-Aware Video Representation Learning by Latent Straightening},
280
+ author={Bagad, Piyush and Zisserman, Andrew},
281
+ journal={arXiv preprint arXiv:2509.08502},
282
+ year={2025}
283
  }
284
  ```
285
 
added_tokens.json CHANGED
@@ -1,3 +1,16 @@
1
  {
2
- "<image>": 32000
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  }
 
1
  {
2
+ "<|box_end|>": 151649,
3
+ "<|box_start|>": 151648,
4
+ "<|endoftext|>": 151643,
5
+ "<|im_end|>": 151645,
6
+ "<|im_start|>": 151644,
7
+ "<|image_pad|>": 151655,
8
+ "<|object_ref_end|>": 151647,
9
+ "<|object_ref_start|>": 151646,
10
+ "<|quad_end|>": 151651,
11
+ "<|quad_start|>": 151650,
12
+ "<|video_pad|>": 151656,
13
+ "<|vision_end|>": 151653,
14
+ "<|vision_pad|>": 151654,
15
+ "<|vision_start|>": 151652
16
  }
chat_template.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "chat_template": "{% set image_count = namespace(value=0) %}{% set video_count = namespace(value=0) %}{% for message in messages %}{% if loop.first and message['role'] != 'system' %}<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n{% endif %}{% if (message['role'] != 'assistant') %}<|im_start|>{{ message['role'] }}\n{% if message['content'] is string %}{{ message['content'] }}<|im_end|>\n{% else %}{% for content in message['content'] %}{% if content['type'] == 'image' %}{% set image_count.value = image_count.value + 1 %}{% if add_vision_id %}Picture {{ image_count.value }}: {% endif %}<|vision_start|>{{'<|image_pad|>' * content['num_vision_tokens']}}<|vision_end|>{% elif content['type'] == 'video' %}{% set video_count.value = video_count.value + 1 %}{% if add_vision_id %}Video {{ video_count.value }}: {% endif %}<|vision_start|>{{'<|image_pad|>' * content['num_vision_tokens']}}<|vision_end|>{% elif content['type'] == 'text' %}{{ content['text'] }}{% endif %}{% endfor %}<|im_end|>\n{% endif %}{% elif (message['role'] == 'assistant') %}<|im_start|>{{ message['role'] }}\n{% generation %}{% if message['content'] is string %}{{ message['content'] }}<|im_end|>\n{% else %}{% for content in message['content'] %}{% if content['type'] == 'image' %}{% set image_count.value = image_count.value + 1 %}{% if add_vision_id %}Picture {{ image_count.value }}: {% endif %}<|vision_start|>{{'<|image_pad|>' * content['num_vision_tokens']}}<|vision_end|>{% elif content['type'] == 'video' %}{% set video_count.value = video_count.value + 1 %}{% if add_vision_id %}Video {{ video_count.value }}: {% endif %}<|vision_start|>{{'<|image_pad|>' * content['num_vision_tokens']}}<|vision_end|>{% elif content['type'] == 'text' %}{{ content['text'] }}{% endif %}{% endfor %}{% if not loop.last or not strip_final_eos %}<|im_end|>\n{% endif %}{% endif %}{% endgeneration %}{% endif %}{% endfor %}{% if add_generation_prompt %}<|im_start|>assistant\n{% endif %}"
3
+ }
config.json CHANGED
@@ -1,27 +1,25 @@
1
  {
2
- "_name_or_path": "/work/piyush/pretrained_checkpoints/Tarsier-7b",
3
- "add_transformer_projector": false,
4
  "architectures": [
5
- "TarsierForConditionalGeneration"
6
  ],
7
  "ignore_index": -100,
8
  "image_new_idx": 32003,
9
  "image_newline_idx": 32002,
10
  "image_seq_length": 576,
11
- "image_token_index": 32000,
12
  "model_type": "llava",
 
13
  "projector_hidden_act": "gelu",
14
  "text_config": {
15
- "_name_or_path": "lmsys/vicuna-7b-v1.5",
 
16
  "add_cross_attention": false,
17
- "architectures": [
18
- "LlamaForCausalLM"
19
- ],
20
- "attention_bias": false,
21
  "attention_dropout": 0.0,
22
  "bad_words_ids": null,
23
  "begin_suppress_tokens": null,
24
- "bos_token_id": 1,
25
  "chunk_size_feed_forward": 0,
26
  "cross_attention_hidden_size": null,
27
  "decoder_start_token_id": null,
@@ -29,20 +27,20 @@
29
  "do_sample": false,
30
  "early_stopping": false,
31
  "encoder_no_repeat_ngram_size": 0,
32
- "eos_token_id": 2,
33
  "exponential_decay_length_penalty": null,
34
  "finetuning_task": null,
35
  "forced_bos_token_id": null,
36
  "forced_eos_token_id": null,
37
- "head_dim": 128,
38
  "hidden_act": "silu",
39
- "hidden_size": 4096,
40
  "id2label": {
41
  "0": "LABEL_0",
42
  "1": "LABEL_1"
43
  },
 
44
  "initializer_range": 0.02,
45
- "intermediate_size": 11008,
46
  "is_decoder": false,
47
  "is_encoder_decoder": false,
48
  "label2id": {
@@ -51,33 +49,42 @@
51
  },
52
  "length_penalty": 1.0,
53
  "max_length": 20,
54
- "max_position_embeddings": 4096,
 
55
  "min_length": 0,
56
- "mlp_bias": false,
57
- "model_type": "llama",
58
  "no_repeat_ngram_size": 0,
59
- "num_attention_heads": 32,
60
  "num_beam_groups": 1,
61
  "num_beams": 1,
62
- "num_hidden_layers": 32,
63
- "num_key_value_heads": 32,
64
  "num_return_sequences": 1,
65
  "output_attentions": false,
66
  "output_hidden_states": false,
67
  "output_scores": false,
68
- "pad_token_id": 0,
69
  "prefix": null,
70
- "pretraining_tp": 1,
71
  "problem_type": null,
72
  "pruned_heads": {},
73
  "remove_invalid_values": false,
74
  "repetition_penalty": 1.0,
75
  "return_dict": true,
76
  "return_dict_in_generate": false,
77
- "rms_norm_eps": 1e-05,
78
- "rope_scaling": null,
79
- "rope_theta": 10000.0,
 
 
 
 
 
 
 
 
80
  "sep_token_id": null,
 
 
81
  "suppress_tokens": null,
82
  "task_specific_params": null,
83
  "temperature": 1.0,
@@ -87,32 +94,112 @@
87
  "tokenizer_class": null,
88
  "top_k": 50,
89
  "top_p": 1.0,
90
- "torch_dtype": "float16",
91
  "torchscript": false,
92
  "typical_p": 1.0,
93
  "use_bfloat16": false,
94
- "use_bpex_rmsnorm": true,
95
- "use_bpex_rotary": true,
96
  "use_cache": false,
97
- "use_ft_flash_attn": true,
98
- "vocab_size": 32064
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  },
100
  "torch_dtype": "bfloat16",
101
- "transformers_version": "4.45.2",
102
  "vision_config": {
 
103
  "_name_or_path": "",
104
  "add_cross_attention": false,
105
  "architectures": null,
106
  "attention_dropout": 0.0,
 
107
  "bad_words_ids": null,
108
  "begin_suppress_tokens": null,
109
  "bos_token_id": null,
110
  "chunk_size_feed_forward": 0,
111
  "cross_attention_hidden_size": null,
112
  "decoder_start_token_id": null,
 
113
  "diversity_penalty": 0.0,
114
  "do_sample": false,
115
  "early_stopping": false,
 
116
  "encoder_no_repeat_ngram_size": 0,
117
  "eos_token_id": null,
118
  "exponential_decay_length_penalty": null,
@@ -120,32 +207,35 @@
120
  "forced_bos_token_id": null,
121
  "forced_eos_token_id": null,
122
  "hidden_act": "quick_gelu",
123
- "hidden_size": 1024,
124
  "id2label": {
125
  "0": "LABEL_0",
126
  "1": "LABEL_1"
127
  },
128
- "image_size": 336,
129
- "initializer_factor": 1.0,
130
  "initializer_range": 0.02,
131
- "intermediate_size": 4096,
132
  "is_decoder": false,
133
  "is_encoder_decoder": false,
134
  "label2id": {
135
  "LABEL_0": 0,
136
  "LABEL_1": 1
137
  },
138
- "layer_norm_eps": 1e-05,
139
  "length_penalty": 1.0,
140
  "max_length": 20,
 
 
141
  "min_length": 0,
142
- "model_type": "clip_vision_model",
 
143
  "no_repeat_ngram_size": 0,
144
- "num_attention_heads": 16,
145
  "num_beam_groups": 1,
146
  "num_beams": 1,
147
- "num_channels": 3,
148
- "num_hidden_layers": 24,
 
149
  "num_return_sequences": 1,
150
  "output_attentions": false,
151
  "output_hidden_states": false,
@@ -154,16 +244,22 @@
154
  "patch_size": 14,
155
  "prefix": null,
156
  "problem_type": null,
157
- "projection_dim": 768,
158
  "pruned_heads": {},
159
  "remove_invalid_values": false,
160
  "repetition_penalty": 1.0,
161
  "return_dict": true,
162
  "return_dict_in_generate": false,
 
 
 
163
  "sep_token_id": null,
 
 
 
164
  "suppress_tokens": null,
165
  "task_specific_params": null,
166
  "temperature": 1.0,
 
167
  "tf_legacy_loss": false,
168
  "tie_encoder_decoder": false,
169
  "tie_word_embeddings": true,
@@ -174,9 +270,83 @@
174
  "torchscript": false,
175
  "typical_p": 1.0,
176
  "use_bfloat16": false,
177
- "vocab_size": 32000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  },
179
  "vision_feature_layer": -2,
180
- "vision_feature_select_strategy": "default",
181
- "vocab_size": 32064
182
  }
 
1
  {
2
+ "_name_or_path": "/work/piyush/pretrained_checkpoints/Tarsier2-7b-0115",
 
3
  "architectures": [
4
+ "Tarsier2ForConditionalGeneration"
5
  ],
6
  "ignore_index": -100,
7
  "image_new_idx": 32003,
8
  "image_newline_idx": 32002,
9
  "image_seq_length": 576,
10
+ "image_token_index": 151655,
11
  "model_type": "llava",
12
+ "projection_head": null,
13
  "projector_hidden_act": "gelu",
14
  "text_config": {
15
+ "_attn_implementation_autoset": true,
16
+ "_name_or_path": "",
17
  "add_cross_attention": false,
18
+ "architectures": null,
 
 
 
19
  "attention_dropout": 0.0,
20
  "bad_words_ids": null,
21
  "begin_suppress_tokens": null,
22
+ "bos_token_id": 151643,
23
  "chunk_size_feed_forward": 0,
24
  "cross_attention_hidden_size": null,
25
  "decoder_start_token_id": null,
 
27
  "do_sample": false,
28
  "early_stopping": false,
29
  "encoder_no_repeat_ngram_size": 0,
30
+ "eos_token_id": 151645,
31
  "exponential_decay_length_penalty": null,
32
  "finetuning_task": null,
33
  "forced_bos_token_id": null,
34
  "forced_eos_token_id": null,
 
35
  "hidden_act": "silu",
36
+ "hidden_size": 3584,
37
  "id2label": {
38
  "0": "LABEL_0",
39
  "1": "LABEL_1"
40
  },
41
+ "image_token_id": 151655,
42
  "initializer_range": 0.02,
43
+ "intermediate_size": 18944,
44
  "is_decoder": false,
45
  "is_encoder_decoder": false,
46
  "label2id": {
 
49
  },
50
  "length_penalty": 1.0,
51
  "max_length": 20,
52
+ "max_position_embeddings": 32768,
53
+ "max_window_layers": 28,
54
  "min_length": 0,
55
+ "model_type": "qwen2_vl",
 
56
  "no_repeat_ngram_size": 0,
57
+ "num_attention_heads": 28,
58
  "num_beam_groups": 1,
59
  "num_beams": 1,
60
+ "num_hidden_layers": 28,
61
+ "num_key_value_heads": 4,
62
  "num_return_sequences": 1,
63
  "output_attentions": false,
64
  "output_hidden_states": false,
65
  "output_scores": false,
66
+ "pad_token_id": null,
67
  "prefix": null,
 
68
  "problem_type": null,
69
  "pruned_heads": {},
70
  "remove_invalid_values": false,
71
  "repetition_penalty": 1.0,
72
  "return_dict": true,
73
  "return_dict_in_generate": false,
74
+ "rms_norm_eps": 1e-06,
75
+ "rope_scaling": {
76
+ "mrope_section": [
77
+ 16,
78
+ 24,
79
+ 24
80
+ ],
81
+ "rope_type": "default",
82
+ "type": "default"
83
+ },
84
+ "rope_theta": 1000000.0,
85
  "sep_token_id": null,
86
+ "sliding_window": 32768,
87
+ "spatial_merge_size": 2,
88
  "suppress_tokens": null,
89
  "task_specific_params": null,
90
  "temperature": 1.0,
 
94
  "tokenizer_class": null,
95
  "top_k": 50,
96
  "top_p": 1.0,
97
+ "torch_dtype": "bfloat16",
98
  "torchscript": false,
99
  "typical_p": 1.0,
100
  "use_bfloat16": false,
101
+ "use_bpex_rotary": false,
 
102
  "use_cache": false,
103
+ "use_sliding_window": false,
104
+ "video_token_id": 151656,
105
+ "vision_config": {
106
+ "_name_or_path": "",
107
+ "add_cross_attention": false,
108
+ "architectures": null,
109
+ "bad_words_ids": null,
110
+ "begin_suppress_tokens": null,
111
+ "bos_token_id": null,
112
+ "chunk_size_feed_forward": 0,
113
+ "cross_attention_hidden_size": null,
114
+ "decoder_start_token_id": null,
115
+ "depth": 32,
116
+ "diversity_penalty": 0.0,
117
+ "do_sample": false,
118
+ "early_stopping": false,
119
+ "embed_dim": 1280,
120
+ "encoder_no_repeat_ngram_size": 0,
121
+ "eos_token_id": null,
122
+ "exponential_decay_length_penalty": null,
123
+ "finetuning_task": null,
124
+ "forced_bos_token_id": null,
125
+ "forced_eos_token_id": null,
126
+ "hidden_act": "quick_gelu",
127
+ "hidden_size": 3584,
128
+ "id2label": {
129
+ "0": "LABEL_0",
130
+ "1": "LABEL_1"
131
+ },
132
+ "in_channels": 3,
133
+ "is_decoder": false,
134
+ "is_encoder_decoder": false,
135
+ "label2id": {
136
+ "LABEL_0": 0,
137
+ "LABEL_1": 1
138
+ },
139
+ "length_penalty": 1.0,
140
+ "max_length": 20,
141
+ "min_length": 0,
142
+ "mlp_ratio": 4,
143
+ "model_type": "qwen2_vl",
144
+ "no_repeat_ngram_size": 0,
145
+ "num_beam_groups": 1,
146
+ "num_beams": 1,
147
+ "num_heads": 16,
148
+ "num_return_sequences": 1,
149
+ "output_attentions": false,
150
+ "output_hidden_states": false,
151
+ "output_scores": false,
152
+ "pad_token_id": null,
153
+ "patch_size": 14,
154
+ "prefix": null,
155
+ "problem_type": null,
156
+ "pruned_heads": {},
157
+ "remove_invalid_values": false,
158
+ "repetition_penalty": 1.0,
159
+ "return_dict": true,
160
+ "return_dict_in_generate": false,
161
+ "sep_token_id": null,
162
+ "spatial_merge_size": 2,
163
+ "suppress_tokens": null,
164
+ "task_specific_params": null,
165
+ "temperature": 1.0,
166
+ "temporal_patch_size": 2,
167
+ "tf_legacy_loss": false,
168
+ "tie_encoder_decoder": false,
169
+ "tie_word_embeddings": true,
170
+ "tokenizer_class": null,
171
+ "top_k": 50,
172
+ "top_p": 1.0,
173
+ "torch_dtype": null,
174
+ "torchscript": false,
175
+ "typical_p": 1.0,
176
+ "use_bfloat16": false
177
+ },
178
+ "vision_end_token_id": 151653,
179
+ "vision_start_token_id": 151652,
180
+ "vision_token_id": 151654,
181
+ "vocab_size": 152064
182
  },
183
  "torch_dtype": "bfloat16",
184
+ "transformers_version": "4.45.0",
185
  "vision_config": {
186
+ "_attn_implementation_autoset": false,
187
  "_name_or_path": "",
188
  "add_cross_attention": false,
189
  "architectures": null,
190
  "attention_dropout": 0.0,
191
+ "attn_implementation": "flash_attention_2",
192
  "bad_words_ids": null,
193
  "begin_suppress_tokens": null,
194
  "bos_token_id": null,
195
  "chunk_size_feed_forward": 0,
196
  "cross_attention_hidden_size": null,
197
  "decoder_start_token_id": null,
198
+ "depth": 32,
199
  "diversity_penalty": 0.0,
200
  "do_sample": false,
201
  "early_stopping": false,
202
+ "embed_dim": 1280,
203
  "encoder_no_repeat_ngram_size": 0,
204
  "eos_token_id": null,
205
  "exponential_decay_length_penalty": null,
 
207
  "forced_bos_token_id": null,
208
  "forced_eos_token_id": null,
209
  "hidden_act": "quick_gelu",
210
+ "hidden_size": 3584,
211
  "id2label": {
212
  "0": "LABEL_0",
213
  "1": "LABEL_1"
214
  },
215
+ "in_channels": 3,
216
+ "in_chans": 3,
217
  "initializer_range": 0.02,
218
+ "intermediate_size": 29568,
219
  "is_decoder": false,
220
  "is_encoder_decoder": false,
221
  "label2id": {
222
  "LABEL_0": 0,
223
  "LABEL_1": 1
224
  },
 
225
  "length_penalty": 1.0,
226
  "max_length": 20,
227
+ "max_position_embeddings": 32768,
228
+ "max_window_layers": 80,
229
  "min_length": 0,
230
+ "mlp_ratio": 4,
231
+ "model_type": "qwen2_vl",
232
  "no_repeat_ngram_size": 0,
233
+ "num_attention_heads": 64,
234
  "num_beam_groups": 1,
235
  "num_beams": 1,
236
+ "num_heads": 16,
237
+ "num_hidden_layers": 80,
238
+ "num_key_value_heads": 8,
239
  "num_return_sequences": 1,
240
  "output_attentions": false,
241
  "output_hidden_states": false,
 
244
  "patch_size": 14,
245
  "prefix": null,
246
  "problem_type": null,
 
247
  "pruned_heads": {},
248
  "remove_invalid_values": false,
249
  "repetition_penalty": 1.0,
250
  "return_dict": true,
251
  "return_dict_in_generate": false,
252
+ "rms_norm_eps": 1e-05,
253
+ "rope_scaling": null,
254
+ "rope_theta": 1000000.0,
255
  "sep_token_id": null,
256
+ "sliding_window": 4096,
257
+ "spatial_merge_size": 2,
258
+ "spatial_patch_size": 14,
259
  "suppress_tokens": null,
260
  "task_specific_params": null,
261
  "temperature": 1.0,
262
+ "temporal_patch_size": 2,
263
  "tf_legacy_loss": false,
264
  "tie_encoder_decoder": false,
265
  "tie_word_embeddings": true,
 
270
  "torchscript": false,
271
  "typical_p": 1.0,
272
  "use_bfloat16": false,
273
+ "use_cache": true,
274
+ "use_sliding_window": false,
275
+ "vision_config": {
276
+ "_name_or_path": "",
277
+ "add_cross_attention": false,
278
+ "architectures": null,
279
+ "bad_words_ids": null,
280
+ "begin_suppress_tokens": null,
281
+ "bos_token_id": null,
282
+ "chunk_size_feed_forward": 0,
283
+ "cross_attention_hidden_size": null,
284
+ "decoder_start_token_id": null,
285
+ "depth": 32,
286
+ "diversity_penalty": 0.0,
287
+ "do_sample": false,
288
+ "early_stopping": false,
289
+ "embed_dim": 1280,
290
+ "encoder_no_repeat_ngram_size": 0,
291
+ "eos_token_id": null,
292
+ "exponential_decay_length_penalty": null,
293
+ "finetuning_task": null,
294
+ "forced_bos_token_id": null,
295
+ "forced_eos_token_id": null,
296
+ "hidden_act": "quick_gelu",
297
+ "hidden_size": 3584,
298
+ "id2label": {
299
+ "0": "LABEL_0",
300
+ "1": "LABEL_1"
301
+ },
302
+ "in_channels": 3,
303
+ "is_decoder": false,
304
+ "is_encoder_decoder": false,
305
+ "label2id": {
306
+ "LABEL_0": 0,
307
+ "LABEL_1": 1
308
+ },
309
+ "length_penalty": 1.0,
310
+ "max_length": 20,
311
+ "min_length": 0,
312
+ "mlp_ratio": 4,
313
+ "model_type": "qwen2_vl",
314
+ "no_repeat_ngram_size": 0,
315
+ "num_beam_groups": 1,
316
+ "num_beams": 1,
317
+ "num_heads": 16,
318
+ "num_return_sequences": 1,
319
+ "output_attentions": false,
320
+ "output_hidden_states": false,
321
+ "output_scores": false,
322
+ "pad_token_id": null,
323
+ "patch_size": 14,
324
+ "prefix": null,
325
+ "problem_type": null,
326
+ "pruned_heads": {},
327
+ "remove_invalid_values": false,
328
+ "repetition_penalty": 1.0,
329
+ "return_dict": true,
330
+ "return_dict_in_generate": false,
331
+ "sep_token_id": null,
332
+ "spatial_merge_size": 2,
333
+ "suppress_tokens": null,
334
+ "task_specific_params": null,
335
+ "temperature": 1.0,
336
+ "temporal_patch_size": 2,
337
+ "tf_legacy_loss": false,
338
+ "tie_encoder_decoder": false,
339
+ "tie_word_embeddings": true,
340
+ "tokenizer_class": null,
341
+ "top_k": 50,
342
+ "top_p": 1.0,
343
+ "torch_dtype": null,
344
+ "torchscript": false,
345
+ "typical_p": 1.0,
346
+ "use_bfloat16": false
347
+ },
348
+ "vocab_size": 152064
349
  },
350
  "vision_feature_layer": -2,
351
+ "vision_feature_select_strategy": "default"
 
352
  }
demo_usage.py CHANGED
@@ -1,6 +1,6 @@
1
  import torch
2
  from termcolor import colored
3
- from modeling_tara import TARA, read_frames_decord, read_images_decord
4
 
5
  import warnings
6
  warnings.filterwarnings("ignore")
@@ -12,36 +12,28 @@ def main(model_path: str = "."):
12
  print(colored("="*60, 'yellow'))
13
 
14
  # Load model from current directory
15
- print(colored("\n[1/6] Loading model...", 'cyan'))
16
  model = TARA.from_pretrained(
17
  model_path, # Load from current directory
18
  device_map='auto',
19
  torch_dtype=torch.bfloat16,
 
20
  )
21
 
22
  n_params = sum(p.numel() for p in model.model.parameters())
23
- print(colored(f"✓ Model loaded successfully!", 'green'))
24
  print(f"Number of parameters: {round(n_params/1e9, 3)}B")
25
  print("-" * 100)
26
 
27
  # Encode a sample video
28
- print(colored("\n[2/6] Testing video encoding and captioning ...", 'cyan'))
29
  video_path = "./assets/folding_paper.mp4"
30
  try:
31
- video_tensor = read_frames_decord(video_path, num_frames=16)
32
- video_tensor = video_tensor.unsqueeze(0)
33
- video_tensor = video_tensor.to(model.model.device)
34
-
35
  with torch.no_grad():
36
- video_emb = model.encode_vision(video_tensor).cpu().squeeze(0).float()
37
-
38
- # Get caption for the video
39
- video_caption = model.describe(video_tensor)[0]
40
 
41
  print(colored("✓ Video encoded successfully!", 'green'))
42
- print(f"Video shape: {video_tensor.shape}") # torch.Size([1, 16, 3, 240, 426])
43
  print(f"Video embedding shape: {video_emb.shape}") # torch.Size([4096])
44
- print(colored(f"Video caption: {video_caption}", 'magenta'))
45
  except FileNotFoundError:
46
  print(colored(f"⚠ Video file not found: {video_path}", 'red'))
47
  print(colored(" Please add a video file or update the path in demo_usage.py", 'yellow'))
@@ -49,7 +41,7 @@ def main(model_path: str = "."):
49
  print("-" * 100)
50
 
51
  # Encode sample texts
52
- print(colored("\n[3/6] Testing text encoding...", 'cyan'))
53
  text = ['someone is folding a paper', 'cutting a paper', 'someone is unfolding a paper']
54
  # NOTE: It can also take a single string
55
 
@@ -62,7 +54,7 @@ def main(model_path: str = "."):
62
 
63
  # Compute similarities if video was encoded
64
  if video_emb is not None:
65
- print(colored("\n[4/6] Computing video-text similarities...", 'cyan'))
66
  similarities = torch.cosine_similarity(
67
  video_emb.unsqueeze(0).unsqueeze(0), # [1, 1, 4096]
68
  text_emb.unsqueeze(0), # [1, 3, 4096]
@@ -76,15 +68,18 @@ def main(model_path: str = "."):
76
 
77
  # Negation example: a negation in text query should result
78
  # in retrieval of images without the neg. object in the query
79
- print(colored("\n[5/6] Testing negation example...", 'cyan'))
80
  image_paths = [
81
  './assets/cat.png',
82
  './assets/dog+cat.png',
83
  ]
84
- image_tensors = read_images_decord(image_paths)
85
- with torch.no_grad():
86
- image_embs = model.encode_vision(image_tensors.to(model.model.device)).cpu().float()
87
- image_embs = torch.nn.functional.normalize(image_embs, dim=-1)
 
 
 
88
  print(f"Image embedding shape: {image_embs.shape}")
89
 
90
  texts = ['an image of a cat but there is no dog in it']
@@ -107,19 +102,17 @@ def main(model_path: str = "."):
107
 
108
 
109
  # Composed video retrieval example
110
- print(colored("\n[6/6] Testing composed video retrieval...", 'cyan'))
111
  # source_video_path = './assets/source-27375787.mp4'
112
  # target_video_path = './assets/target-27387901.mp4'
113
  # edit_text = "Make the billboard blank"
114
  source_video_path = "./assets/5369546.mp4"
115
  target_video_path = "./assets/1006630957.mp4"
116
  edit_text ="make the tree lit up"
117
- source_video_tensor = read_frames_decord(source_video_path, num_frames=4)
118
- target_video_tensor = read_frames_decord(target_video_path, num_frames=16)
119
  with torch.no_grad():
120
- source_video_emb = model.encode_vision(source_video_tensor.unsqueeze(0), edit_text).cpu().squeeze(0).float()
121
  source_video_emb = torch.nn.functional.normalize(source_video_emb, dim=-1)
122
- target_video_emb = model.encode_vision(target_video_tensor.unsqueeze(0)).cpu().squeeze(0).float()
123
  target_video_emb = torch.nn.functional.normalize(target_video_emb, dim=-1)
124
  sim_with_edit = source_video_emb @ target_video_emb.t()
125
  print(f"Source-Target similarity with edit: {sim_with_edit}")
@@ -133,7 +126,11 @@ def main(model_path: str = "."):
133
  if __name__ == "__main__":
134
  import argparse
135
  parser = argparse.ArgumentParser()
136
- parser.add_argument("--model_path", type=str, default=".")
137
  args = parser.parse_args()
138
 
139
- main(args.model_path)
 
 
 
 
 
1
  import torch
2
  from termcolor import colored
3
+
4
 
5
  import warnings
6
  warnings.filterwarnings("ignore")
 
12
  print(colored("="*60, 'yellow'))
13
 
14
  # Load model from current directory
15
+ print(colored("\n[1/5] Loading model...", 'cyan'))
16
  model = TARA.from_pretrained(
17
  model_path, # Load from current directory
18
  device_map='auto',
19
  torch_dtype=torch.bfloat16,
20
+ attn_implementation='flash_attention_2',
21
  )
22
 
23
  n_params = sum(p.numel() for p in model.model.parameters())
24
+ print(colored("✓ Model loaded successfully!", 'green'))
25
  print(f"Number of parameters: {round(n_params/1e9, 3)}B")
26
  print("-" * 100)
27
 
28
  # Encode a sample video
29
+ print(colored("\n[2/5] Testing video encoding ...", 'cyan'))
30
  video_path = "./assets/folding_paper.mp4"
31
  try:
 
 
 
 
32
  with torch.no_grad():
33
+ video_emb = model.encode_vision(video_path).cpu().squeeze(0).float()
 
 
 
34
 
35
  print(colored("✓ Video encoded successfully!", 'green'))
 
36
  print(f"Video embedding shape: {video_emb.shape}") # torch.Size([4096])
 
37
  except FileNotFoundError:
38
  print(colored(f"⚠ Video file not found: {video_path}", 'red'))
39
  print(colored(" Please add a video file or update the path in demo_usage.py", 'yellow'))
 
41
  print("-" * 100)
42
 
43
  # Encode sample texts
44
+ print(colored("\n[3/5] Testing text encoding...", 'cyan'))
45
  text = ['someone is folding a paper', 'cutting a paper', 'someone is unfolding a paper']
46
  # NOTE: It can also take a single string
47
 
 
54
 
55
  # Compute similarities if video was encoded
56
  if video_emb is not None:
57
+ print(colored("\n[4/5] Computing video-text similarities...", 'cyan'))
58
  similarities = torch.cosine_similarity(
59
  video_emb.unsqueeze(0).unsqueeze(0), # [1, 1, 4096]
60
  text_emb.unsqueeze(0), # [1, 3, 4096]
 
68
 
69
  # Negation example: a negation in text query should result
70
  # in retrieval of images without the neg. object in the query
71
+ print(colored("\n[5/5] Testing negation example...", 'cyan'))
72
  image_paths = [
73
  './assets/cat.png',
74
  './assets/dog+cat.png',
75
  ]
76
+ image_embs = []
77
+ for image_path in image_paths:
78
+ with torch.no_grad():
79
+ image_emb = model.encode_vision(image_path).cpu().float()
80
+ image_embs.append(image_emb)
81
+ image_embs = torch.cat(image_embs, dim=0)
82
+ image_embs = torch.nn.functional.normalize(image_embs, dim=-1)
83
  print(f"Image embedding shape: {image_embs.shape}")
84
 
85
  texts = ['an image of a cat but there is no dog in it']
 
102
 
103
 
104
  # Composed video retrieval example
105
+ print(colored("\n[Bonus] Testing composed video retrieval...", 'cyan'))
106
  # source_video_path = './assets/source-27375787.mp4'
107
  # target_video_path = './assets/target-27387901.mp4'
108
  # edit_text = "Make the billboard blank"
109
  source_video_path = "./assets/5369546.mp4"
110
  target_video_path = "./assets/1006630957.mp4"
111
  edit_text ="make the tree lit up"
 
 
112
  with torch.no_grad():
113
+ source_video_emb = model.encode_vision_with_text(source_video_path, edit_text).cpu().squeeze(0).float()
114
  source_video_emb = torch.nn.functional.normalize(source_video_emb, dim=-1)
115
+ target_video_emb = model.encode_vision(target_video_path).cpu().squeeze(0).float()
116
  target_video_emb = torch.nn.functional.normalize(target_video_emb, dim=-1)
117
  sim_with_edit = source_video_emb @ target_video_emb.t()
118
  print(f"Source-Target similarity with edit: {sim_with_edit}")
 
126
  if __name__ == "__main__":
127
  import argparse
128
  parser = argparse.ArgumentParser()
129
+ parser.add_argument("--model_path", type=str, default="/work/piyush/pretrained_checkpoints/Tarsier2-7b-0115/")
130
  args = parser.parse_args()
131
 
132
+ # import sys
133
+ # sys.path.append(args.model_path)
134
+ from modeling_tara import TARA
135
+
136
+ main(args.model_path)
generation_config.json CHANGED
@@ -1,8 +1,6 @@
1
  {
2
  "_from_model_config": true,
3
- "bos_token_id": 1,
4
- "eos_token_id": 2,
5
- "pad_token_id": 0,
6
- "transformers_version": "4.45.2",
7
- "use_cache": false
8
  }
 
1
  {
2
  "_from_model_config": true,
3
+ "bos_token_id": 151643,
4
+ "eos_token_id": 151645,
5
+ "transformers_version": "4.45.0"
 
 
6
  }
merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
model-00001-of-00003.safetensors → model-00001-of-00004.safetensors RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:21e95f82edbfe05e3e2d55e1c76a3b56a4b29f5da9f7d7f7f7e8ed6ef29c9602
3
- size 4992930688
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f2c855a92271b7d4f6510eb0274ab3c5049ab98871c501da10845d0a6b83a8b2
3
+ size 4966663320
model-00002-of-00003.safetensors → model-00002-of-00004.safetensors RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:12847ca907e67d2bc044f2c1741c7b43e25aecd2ac32a663f4de8d16dbaaed0f
3
- size 4957878552
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:96ae9d84f6e61e177d4f6bdaf7a7ee16e21af31e213f357b2862cc66b78fe481
3
+ size 4991497784
model-00003-of-00003.safetensors → model-00003-of-00004.safetensors RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:58d5999ca44b53916bc916d3d135376b420f5db31763e6b623b1705207865837
3
- size 4176137496
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8c51f1448e8650e932981b25398957691c0b3a16926ba944f6d4aff2a8599342
3
+ size 4932752872
tokenizer.model → model-00004-of-00004.safetensors RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:9e556afd44213b6bd1be2b850ebbbd98f5481437a8021afaf58ee7fb1818d347
3
- size 499723
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c98c3be29d5f182a6b18bfe72139c87a53aa0ff7287678d63faa10af4d09f0d4
3
+ size 1691924640
model.safetensors.index.json CHANGED
The diff for this file is too large to render. See raw diff
 
modeling_tara.py CHANGED
@@ -2,57 +2,34 @@ import os
2
  from abc import ABCMeta, abstractmethod
3
  from typing import Optional, Union, Dict, List
4
  from termcolor import colored
5
- import random
6
 
7
-
8
- import numpy as np
9
  import torch
10
  from transformers import (
11
- AutoProcessor,
12
- AutoTokenizer,
13
  LlavaConfig,
14
- LlamaForCausalLM,
15
- )
16
- from torchvision.transforms.v2 import (
17
- ToPILImage,
18
  )
19
  import decord
20
- from decord import VideoReader
 
 
 
 
21
 
22
  decord.bridge.set_bridge("torch")
23
 
24
- # TODO: need to use these directly
25
- from tarsier.modeling_tarsier import TarsierForConditionalGeneration
26
- from tarsier.processor import Processor
27
- # from utils.model import transform_pixel_values
28
-
29
 
30
  EOL_PROMPTS = {
31
  'text': '<sent>\nSummary above sentence in one word:',
32
  'image': '<image>\nSummary above image in one word:',
33
  'video': '<video>\nSummary above video in one word:',
 
 
 
 
34
  }
 
 
35
 
36
 
37
- def transform_pixel_values(pixel_values: torch.Tensor | List[torch.Tensor]) -> torch.Tensor:
38
- # NOTE: this function doesn't accept unbatched inputs
39
- # pixel_values should be uint8 of (B, T, C, H, W)
40
- if isinstance(pixel_values, list):
41
- pixel_values = torch.stack(pixel_values)
42
-
43
- if pixel_values.ndim == 4:
44
- # pixel_values is (B, C, H, W)
45
- # (B, C, H, W) -> (B, 1, C, H, W)
46
- pixel_values = pixel_values.unsqueeze(1)
47
- elif pixel_values.ndim == 5:
48
- # pixel_values is (B, T, C, H, W)
49
- pass
50
- else:
51
- raise ValueError(f"pixel_values should be 4D or 5D, got {pixel_values.ndim}D")
52
- return pixel_values
53
-
54
-
55
- base_registry = {}
56
  class BaseModel(metaclass=ABCMeta):
57
  def __init_subclass__(cls, **kwargs):
58
  super().__init_subclass__(**kwargs)
@@ -67,16 +44,65 @@ class BaseModel(metaclass=ABCMeta):
67
  load_llm: bool = False,
68
  device_map: Optional[Union[str, Dict[str, int]]] = None,
69
  **kwargs):
70
- print(colored(f'[ MODEL ] Loading {cls.__name__} from {model_name_or_path} [..............]', 'yellow'))
71
 
72
  return cls(model_name_or_path, load_llm=load_llm, device_map=device_map, **kwargs)
73
 
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  class BaseModelForTARA(BaseModel):
76
 
77
- ARCHITECTURE = "TarsierForConditionalGeneration"
78
- LLM_CLASS = LlamaForCausalLM
79
- MLLM_CLASS = TarsierForConditionalGeneration
80
 
81
  @property
82
  def describe_prompt(self):
@@ -84,27 +110,38 @@ class BaseModelForTARA(BaseModel):
84
 
85
  @property
86
  def text_eol_prompt(self):
87
- prompt = f'USER: {EOL_PROMPTS["text"]} ASSISTANT: '
 
88
  return prompt
89
 
90
  @property
91
  def image_eol_prompt(self):
92
- prompt = f'USER: {EOL_PROMPTS["image"]} ASSISTANT: '
 
93
  return prompt
94
 
95
  @property
96
  def video_eol_prompt(self):
97
- prompt = f'USER: {EOL_PROMPTS["video"]} ASSISTANT: '
 
98
  return prompt
99
 
100
- @property
101
- def video_edit_eol_prompt(self):
102
- prompt = "Source video: <video>\nEdit instruction: <sent>\n"\
103
- "Look at the attached video carefully. The provided text is instruction to edit the video. "\
104
- "Imagine this edit instruction being applied to the provided video frame.\n"\
105
- "Summarize the resulting edited video in one word:"
106
- prompt = f"USER: {prompt} ASSISTANT: "
107
- return prompt
 
 
 
 
 
 
 
 
108
 
109
  def __init__(
110
  self,
@@ -120,27 +157,61 @@ class BaseModelForTARA(BaseModel):
120
  self.split_weights(model_name_or_path, model_name_or_path + '-llm')
121
  model_name_or_path += '-llm'
122
  model_config = None
123
- self.processor = AutoProcessor.from_pretrained(model_name_or_path, use_fast=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  else:
125
  model_config = LlavaConfig.from_pretrained(
126
  model_name_or_path,
127
- # trust_remote_code=True,
128
  )
129
- self.processor = Processor(
130
- model_name_or_path,
131
- max_n_frames=32,
 
 
 
 
 
 
 
 
 
132
  )
133
-
134
- self.tokenizer = self.processor.tokenizer
 
 
 
 
 
135
 
136
  self.model = MODEL_CLASS.from_pretrained(
137
  model_name_or_path,
138
  config=model_config,
139
- torch_dtype=kwargs.get("torch_dtype", torch.bfloat16),
 
 
140
  device_map=device_map,
141
- # trust_remote_code=True
 
142
  )
143
 
 
 
 
144
  self.model.eval()
145
 
146
  def split_weights(self, mllm_path, llm_path):
@@ -148,176 +219,167 @@ class BaseModelForTARA(BaseModel):
148
  print(f'{llm_path} already exists. Skip splitting weights.')
149
  return
150
  print('Splitting LLM weights from MLLM.')
151
- model = self.MLLM_CLASS.from_pretrained(mllm_path)
 
 
 
 
 
152
  llm = model.language_model
153
- processor = AutoProcessor.from_pretrained(mllm_path)
154
- tokenizer = AutoTokenizer.from_pretrained(mllm_path)
155
  llm.save_pretrained(llm_path)
156
- processor.save_pretrained(llm_path)
157
- tokenizer.save_pretrained(llm_path)
158
-
159
-
160
- encoder_registry = {}
161
- class EncodeMixin(metaclass=ABCMeta):
162
- def __init_subclass__(cls, **kwargs):
163
- super().__init_subclass__(**kwargs)
164
- # register model architecture
165
- if hasattr(cls, 'ARCHITECTURE'):
166
- encoder_registry[cls.ARCHITECTURE] = cls
167
-
168
- @abstractmethod
169
- def encode_vision(self, pixel_values: torch.Tensor | List[torch.Tensor]) -> torch.Tensor:
170
- """
171
- Encodes vision data (images or videos) into a tensor representation.
172
-
173
- Args:
174
- pixel_values (torch.Tensor | List[torch.Tensor]): The input pixel values.
175
- - If a tensor, it should be of shape (B, C, H, W) for images or (B, T, C, H, W) for videos.
176
- - If a list, it will be stacked into a tensor.
177
-
178
- Returns:
179
- torch.Tensor: The encoded tensor representation of the input vision data.
180
-
181
- Raises:
182
- ValueError: If `pixel_values` is not 4D or 5D.
183
-
184
- ## Notes:
185
- - This function does not accept unbatched inputs.
186
- - `pixel_values` should be of type uint8.
187
- """
188
- raise NotImplementedError
189
-
190
- @abstractmethod
191
- def encode_text(self, text: str | List[str]) -> torch.Tensor:
192
- """
193
- Encodes the given text(s) into a tensor representation using the model.
194
-
195
- Args:
196
- text (str | List[str]): A single string or a list of strings to be encoded.
197
-
198
- Returns:
199
- torch.Tensor: The tensor representation of the encoded text(s).
200
-
201
- ## Notes:
202
- - The method uses a prompt to encode the text.
203
- - If a single string is provided, it is converted into a list containing that string.
204
- - The method processes the prompts and generates the tensor representation using the model.
205
- - The output tensor contains the hidden states of the last token for each input text.
206
- """
207
- raise NotImplementedError
208
 
209
 
210
  class TARA(BaseModelForTARA, EncodeMixin):
211
-
212
- def encode_vision(self, pixel_values: torch.Tensor | List[torch.Tensor], edit_text: Optional[str] = None) -> torch.Tensor:
213
-
214
- pixel_values = transform_pixel_values(pixel_values) # [B, T, C, H, W]
215
- nframes = pixel_values.shape[1]
216
 
217
- if edit_text is not None:
218
- # For composed video retrieval, we need to embed a video with the given text
219
- prompt = self.video_edit_eol_prompt.replace('<sent>', edit_text)
220
  else:
221
- prompt = self.image_eol_prompt if nframes == 1 else self.video_eol_prompt
222
 
223
- to_image = ToPILImage()
224
- batched_frames = []
225
- for batch in pixel_values:
226
- frames = [to_image(v) for v in batch]
227
- batched_frames.append(frames)
228
-
229
- generate_kwargs = {
230
- "max_new_tokens": 1,
231
- "output_hidden_states": True,
232
- "return_dict_in_generate": True,
233
- }
234
-
235
- vision_embs = []
236
-
237
- for frames in batched_frames:
238
- input_prompt = prompt.replace("<video>", "<image>"*len(frames))
239
- input_ids = self.processor.get_text_inputs(input_prompt)
240
- frames = self.processor.get_pixel_values(frames)
241
- inputs = {
242
- "input_ids": input_ids,
243
- "pixel_values": frames
244
- }
245
- inputs = {k:v.to(self.model.device) for k,v in inputs.items() if v is not None}
246
- outputs = self.model.generate(
247
- **inputs,
248
- **generate_kwargs,
249
  )
250
- vision_embs.append(outputs.hidden_states[0][-1][:, -1, :])
251
-
252
- vision_embs = torch.cat(vision_embs)
253
- return vision_embs
254
 
255
- def encode_text(self, text: str | List[str]) -> torch.Tensor:
256
-
257
- prompt = self.text_eol_prompt
258
-
259
- if isinstance(text, str):
260
- text = [text]
261
-
262
- prompts = [prompt.replace('<sent>', t) for t in text]
263
-
264
- generate_kwargs = {
265
- "max_new_tokens": 1,
266
- "output_hidden_states": True,
267
- "return_dict_in_generate": True,
268
- }
269
-
270
- text_embs = []
271
-
272
- for p in prompts:
273
- text_inputs = self.processor.get_text_inputs(p)
274
- inputs = {
275
- "input_ids": text_inputs,
276
- }
277
- inputs = {k:v.to(self.model.device) for k,v in inputs.items() if v is not None}
278
- outputs = self.model.generate(
279
- **inputs,
280
- **generate_kwargs,
281
  )
282
- text_embs.append(outputs.hidden_states[0][-1][:, -1, :])
283
-
284
- text_embs = torch.cat(text_embs)
285
- return text_embs
286
-
287
- def describe(self, pixel_values: torch.Tensor | List[torch.Tensor]) -> List[str]:
288
-
289
- pixel_values = transform_pixel_values(pixel_values) # [B, T, C, H, W]
290
- to_image = ToPILImage()
291
- batched_frames = []
292
- for batch in pixel_values:
293
- frames = [to_image(v) for v in batch]
294
- batched_frames.append(frames)
295
- descriptions = []
296
- generate_kwargs = {
297
- "do_sample": False,
298
- "max_new_tokens": 2048,
299
- "top_p": 1,
300
- "temperature": 0,
301
- "use_cache": True
302
- }
303
-
304
- for frames in batched_frames:
305
- text_inputs = f"<video>\n{self.describe_prompt}"
306
- text_inputs = self.processor.process_prompt(text_inputs, frames)
307
- text_inputs = self.processor.get_text_inputs(text_inputs)
308
- frames = self.processor.get_pixel_values(frames)
309
- inputs = {
310
- "input_ids": text_inputs,
311
- "pixel_values": frames
312
- }
313
- inputs = {k:v.to(self.model.device) for k,v in inputs.items() if v is not None}
314
- outputs = self.model.generate(
315
- **inputs,
316
- **generate_kwargs,
317
  )
318
- output_text = self.processor.tokenizer.decode(outputs[0][inputs['input_ids'][0].shape[0]:], skip_special_tokens=True)
319
- descriptions.append(output_text)
320
- return descriptions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
 
323
  def get_frame_indices(num_frames, vlen, sample='rand', fix_start=None, input_fps=1, max_num_frames=-1):
@@ -396,7 +458,6 @@ def read_frames_decord(
396
  del video_reader
397
 
398
 
399
- import PIL.Image
400
  def read_image_decord(image_path):
401
  image = PIL.Image.open(image_path)
402
  image = image.convert('RGB')
@@ -417,10 +478,14 @@ def read_images_decord(image_paths):
417
 
418
 
419
  if __name__ == "__main__":
 
 
 
 
420
 
421
  # Load model
422
  model = TARA.from_pretrained(
423
- "/work/piyush/experiments/CaRe/Tarsier-7b/final-10112025/nli_9000+ego_1000+subj_replaced-seed_42/merged_checkpoint",
424
  device_map='auto',
425
  dtype=torch.bfloat16,
426
  )
@@ -430,12 +495,12 @@ if __name__ == "__main__":
430
  # Let's encode a sample video
431
  print(colored("Testing video encoding...", 'cyan'))
432
  video_path = "./assets/folding_paper.mp4"
433
- video_tensor = read_frames_decord(video_path, num_frames=16)
434
- video_tensor = video_tensor.unsqueeze(0)
435
- video_tensor = video_tensor.to(model.model.device)
436
  with torch.no_grad():
437
- video_emb = model.encode_vision(video_tensor).cpu().squeeze(0).float()
438
- print("Video shape:", video_tensor.shape) # torch.Size([1, 16, 3, 240, 426])
439
  print("Video embedding shape:", video_emb.shape) # torch.Size([4096])
440
 
441
  # Let's encode a sample text
 
2
  from abc import ABCMeta, abstractmethod
3
  from typing import Optional, Union, Dict, List
4
  from termcolor import colored
 
5
 
 
 
6
  import torch
7
  from transformers import (
 
 
8
  LlavaConfig,
 
 
 
 
9
  )
10
  import decord
11
+ import PIL.Image
12
+ from tarsier2.dataset.utils import format_one_sample
13
+ from tarsier2.modeling_tarsier2 import Tarsier2ForConditionalGeneration
14
+ from tarsier2.modeling_qwen2_vl_fast import Qwen2VLForCausalLM
15
+ from tarsier2.dataset.tarsier_datamodule import init_processor
16
 
17
  decord.bridge.set_bridge("torch")
18
 
 
 
 
 
 
19
 
20
  EOL_PROMPTS = {
21
  'text': '<sent>\nSummary above sentence in one word:',
22
  'image': '<image>\nSummary above image in one word:',
23
  'video': '<video>\nSummary above video in one word:',
24
+ "video_edit": "USER: Source video: <video>\nEdit instruction: <sent>\n"\
25
+ "Look at the attached video carefully. The provided text is instruction to edit the video. "\
26
+ "Imagine this edit instruction being applied to the provided video frame.\n"\
27
+ "Summarize the resulting edited video in one word: ASSISTANT:"
28
  }
29
+ base_registry = {}
30
+ encoder_registry = {}
31
 
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  class BaseModel(metaclass=ABCMeta):
34
  def __init_subclass__(cls, **kwargs):
35
  super().__init_subclass__(**kwargs)
 
44
  load_llm: bool = False,
45
  device_map: Optional[Union[str, Dict[str, int]]] = None,
46
  **kwargs):
47
+ colored(f'Loading {cls.__name__} from {model_name_or_path}')
48
 
49
  return cls(model_name_or_path, load_llm=load_llm, device_map=device_map, **kwargs)
50
 
51
 
52
+ class EncodeMixin(metaclass=ABCMeta):
53
+ def __init_subclass__(cls, **kwargs):
54
+ super().__init_subclass__(**kwargs)
55
+ # register model architecture
56
+ if hasattr(cls, 'ARCHITECTURE'):
57
+ encoder_registry[cls.ARCHITECTURE] = cls
58
+
59
+ @abstractmethod
60
+ def encode_vision(self, pixel_values: torch.Tensor | List[torch.Tensor]) -> torch.Tensor:
61
+ """
62
+ Encodes vision data (images or videos) into a tensor representation.
63
+
64
+ Args:
65
+ pixel_values (torch.Tensor | List[torch.Tensor]): The input pixel values.
66
+ - If a tensor, it should be of shape (B, C, H, W) for images or (B, T, C, H, W) for videos.
67
+ - If a list, it will be stacked into a tensor.
68
+
69
+ Returns:
70
+ torch.Tensor: The encoded tensor representation of the input vision data.
71
+
72
+ Raises:
73
+ ValueError: If `pixel_values` is not 4D or 5D.
74
+
75
+ ## Notes:
76
+ - This function does not accept unbatched inputs.
77
+ - `pixel_values` should be of type uint8.
78
+ """
79
+ raise NotImplementedError
80
+
81
+ @abstractmethod
82
+ def encode_text(self, text: str | List[str]) -> torch.Tensor:
83
+ """
84
+ Encodes the given text(s) into a tensor representation using the model.
85
+
86
+ Args:
87
+ text (str | List[str]): A single string or a list of strings to be encoded.
88
+
89
+ Returns:
90
+ torch.Tensor: The tensor representation of the encoded text(s).
91
+
92
+ ## Notes:
93
+ - The method uses a prompt to encode the text.
94
+ - If a single string is provided, it is converted into a list containing that string.
95
+ - The method processes the prompts and generates the tensor representation using the model.
96
+ - The output tensor contains the hidden states of the last token for each input text.
97
+ """
98
+ raise NotImplementedError
99
+
100
+
101
  class BaseModelForTARA(BaseModel):
102
 
103
+ ARCHITECTURE = "Tarsier2ForConditionalGeneration"
104
+ LLM_CLASS = Qwen2VLForCausalLM
105
+ MLLM_CLASS = Tarsier2ForConditionalGeneration
106
 
107
  @property
108
  def describe_prompt(self):
 
110
 
111
  @property
112
  def text_eol_prompt(self):
113
+ # prompt = f'USER: {EOL_PROMPTS["text"]} ASSISTANT: '
114
+ prompt = EOL_PROMPTS["text"]
115
  return prompt
116
 
117
  @property
118
  def image_eol_prompt(self):
119
+ # prompt = f'USER: {EOL_PROMPTS["image"]} ASSISTANT: '
120
+ prompt = EOL_PROMPTS["image"]
121
  return prompt
122
 
123
  @property
124
  def video_eol_prompt(self):
125
+ # prompt = f'USER: {EOL_PROMPTS["video"]} ASSISTANT: '
126
+ prompt = EOL_PROMPTS["video"]
127
  return prompt
128
 
129
+ @staticmethod
130
+ def _resolve_attn_implementation(requested_attn_impl: Optional[str] = None) -> str:
131
+ attn_impl = requested_attn_impl or "flash_attention_2"
132
+ if attn_impl != "flash_attention_2":
133
+ return attn_impl
134
+ if not torch.cuda.is_available():
135
+ print("CUDA is unavailable; falling back attn_implementation to 'eager'.")
136
+ return "eager"
137
+ major, _ = torch.cuda.get_device_capability(torch.cuda.current_device())
138
+ if major < 8:
139
+ print(
140
+ f"GPU compute capability {major}.x does not support FlashAttention-2; "
141
+ "falling back attn_implementation to 'eager'."
142
+ )
143
+ return "eager"
144
+ return "flash_attention_2"
145
 
146
  def __init__(
147
  self,
 
157
  self.split_weights(model_name_or_path, model_name_or_path + '-llm')
158
  model_name_or_path += '-llm'
159
  model_config = None
160
+
161
+ # from tarsier2.tarsier2_processor import TarsierProcessor
162
+ # self.processor = TarsierProcessor.from_pretrained(model_name_or_path, use_fast=False)
163
+ # self.tokenizer = self.processor.tokenizer
164
+
165
+ import shared.utils as su
166
+ self.base_config = su.io.load_yml(
167
+ os.path.join(
168
+ su.log.repo_path, 'tarsier2/default_config.yaml'
169
+ )
170
+ )
171
+ self.super_processor = init_processor(model_name_or_path, self.base_config)
172
+ self.processor = self.super_processor.processor
173
+ self.tokenizer = self.processor.tokenizer
174
+
175
  else:
176
  model_config = LlavaConfig.from_pretrained(
177
  model_name_or_path,
178
+ trust_remote_code=True,
179
  )
180
+ # from tarsier2.tarsier2_processor import TarsierProcessor
181
+ # self.processor = TarsierProcessor.from_pretrained(
182
+ # model_name_or_path,
183
+ # padding_side='left',
184
+ # trust_remote_code=True,
185
+ # )
186
+ # Load base config
187
+ import shared.utils as su
188
+ self.base_config = su.io.load_yml(
189
+ os.path.join(
190
+ su.log.repo_path, 'tarsier2/default_config.yaml'
191
+ )
192
  )
193
+ self.super_processor = init_processor(model_name_or_path, self.base_config)
194
+ self.processor = self.super_processor.processor
195
+ self.tokenizer = self.processor.tokenizer
196
+
197
+ attn_implementation = self._resolve_attn_implementation(
198
+ kwargs.get("attn_implementation", "flash_attention_2")
199
+ )
200
 
201
  self.model = MODEL_CLASS.from_pretrained(
202
  model_name_or_path,
203
  config=model_config,
204
+ attn_implementation=attn_implementation,
205
+ # torch_dtype=kwargs.get("torch_dtype", torch.bfloat16),
206
+ torch_dtype=torch.bfloat16,
207
  device_map=device_map,
208
+ trust_remote_code=True,
209
+ low_cpu_mem_usage=kwargs.get("low_cpu_mem_usage", True), # Default to True for large models
210
  )
211
 
212
+ # self.processor.patch_size = self.model.config.vision_config.patch_size
213
+ # self.processor.vision_feature_select_strategy = self.model.config.vision_feature_select_strategy
214
+
215
  self.model.eval()
216
 
217
  def split_weights(self, mllm_path, llm_path):
 
219
  print(f'{llm_path} already exists. Skip splitting weights.')
220
  return
221
  print('Splitting LLM weights from MLLM.')
222
+ attn_implementation = self._resolve_attn_implementation("flash_attention_2")
223
+ model = self.MLLM_CLASS.from_pretrained(
224
+ mllm_path,
225
+ attn_implementation=attn_implementation,
226
+ torch_dtype=torch.bfloat16,
227
+ )
228
  llm = model.language_model
 
 
229
  llm.save_pretrained(llm_path)
230
+
231
+ import shared.utils as su
232
+ from tarsier2.dataset.tarsier_datamodule import init_processor
233
+ base_config = su.io.load_yml(
234
+ os.path.join(su.log.repo_path, 'models/tarsier2/default_config.yaml'),
235
+ )
236
+ super_processor = init_processor(
237
+ mllm_path,
238
+ base_config,
239
+ )
240
+ super_processor.processor.save_pretrained(llm_path)
241
+ super_processor.processor.tokenizer.save_pretrained(llm_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
 
244
  class TARA(BaseModelForTARA, EncodeMixin):
245
+
246
+ def encode_vision(self, video_path: str, prompt=None) -> torch.Tensor:
 
 
 
247
 
248
+ ext = video_path.split('.')[-1]
249
+ if ext in ['mp4', 'avi', 'mov', 'mkv', 'webm']:
250
+ is_video = True
251
  else:
252
+ is_video = False
253
 
254
+ if prompt is None:
255
+ if is_video:
256
+ prompt = self.video_eol_prompt
257
+ else:
258
+ prompt = self.image_eol_prompt
259
+ else:
260
+ assert "<video>" in prompt or "<image>" in prompt
261
+ sample = format_one_sample(media_file=video_path, prompt=prompt)
262
+ sample = self.super_processor(sample)
263
+ model_inputs = {}
264
+ for k, v in sample.items():
265
+ if not isinstance(v, torch.Tensor):
266
+ continue
267
+ model_inputs[k] = v.to(self.model.device)
268
+ with torch.inference_mode():
269
+ output = self.model.generate(
270
+ **model_inputs,
271
+ max_new_tokens=1,
272
+ output_hidden_states=True,
273
+ return_dict_in_generate=True,
274
+ pad_token_id=self.processor.tokenizer.eos_token_id
 
 
 
 
 
275
  )
276
+ emb = output.hidden_states[0][-1][:, -1, :]
277
+ return emb
 
 
278
 
279
+ def encode_vision_with_text(self, video_path: str, text: str) -> torch.Tensor:
280
+ ext = video_path.split('.')[-1]
281
+ # if ext in ['mp4', 'avi', 'mov', 'mkv', 'webm']:
282
+ # is_video = True
283
+ # else:
284
+ # is_video = False
285
+ # assert not is_video
286
+ prompt = EOL_PROMPTS["video_edit"].replace('<sent>', text)
287
+ sample = format_one_sample(media_file=video_path, prompt=prompt)
288
+ sample = self.super_processor(sample)
289
+ model_inputs = {}
290
+ for k, v in sample.items():
291
+ if not isinstance(v, torch.Tensor):
292
+ continue
293
+ model_inputs[k] = v.to(self.model.device)
294
+ with torch.inference_mode():
295
+ output = self.model.generate(
296
+ **model_inputs,
297
+ max_new_tokens=1,
298
+ output_hidden_states=True,
299
+ return_dict_in_generate=True,
300
+ pad_token_id=self.processor.tokenizer.eos_token_id
 
 
 
 
301
  )
302
+ emb = output.hidden_states[0][-1][:, -1, :]
303
+ return emb
304
+
305
+ def encode_image(self, image_path: str, prompt=None):
306
+ ext = image_path.split('.')[-1]
307
+ if ext in ['mp4', 'avi', 'mov', 'mkv', 'webm']:
308
+ is_video = True
309
+ else:
310
+ is_video = False
311
+ assert not is_video
312
+ if prompt is None:
313
+ prompt = self.image_eol_prompt
314
+ else:
315
+ assert "<image>" in prompt
316
+ sample = format_one_sample(media_file=image_path, prompt=prompt)
317
+ sample = self.super_processor(sample)
318
+ model_inputs = {}
319
+ for k, v in sample.items():
320
+ if not isinstance(v, torch.Tensor):
321
+ continue
322
+ model_inputs[k] = v.to(self.model.device)
323
+ with torch.inference_mode():
324
+ output = self.model.generate(
325
+ **model_inputs,
326
+ max_new_tokens=1,
327
+ output_hidden_states=True,
328
+ return_dict_in_generate=True,
329
+ pad_token_id=self.processor.tokenizer.eos_token_id
 
 
 
 
 
 
 
330
  )
331
+ emb = output.hidden_states[0][-1][:, -1, :]
332
+ return emb
333
+
334
+ def encode_text(self, text: str, prompt=None) -> torch.Tensor:
335
+
336
+ if prompt is None:
337
+ prompt = self.text_eol_prompt
338
+ else:
339
+ assert "<sent>" in prompt
340
+
341
+ if isinstance(text, str):
342
+ prompt = prompt.replace('<sent>', text)
343
+ sample = format_one_sample(media_file=None, prompt=prompt)
344
+ sample = self.super_processor(sample)
345
+ model_inputs = {}
346
+ for k, v in sample.items():
347
+ if not isinstance(v, torch.Tensor):
348
+ continue
349
+ model_inputs[k] = v.to(self.model.device)
350
+ with torch.inference_mode():
351
+ output = self.model.generate(
352
+ **model_inputs,
353
+ max_new_tokens=1,
354
+ output_hidden_states=True,
355
+ return_dict_in_generate=True,
356
+ pad_token_id=self.processor.tokenizer.eos_token_id
357
+ )
358
+ emb = output.hidden_states[0][-1][:, -1, :]
359
+ return emb
360
+ elif isinstance(text, list):
361
+ text_embs = []
362
+ for t in text:
363
+ prompt = self.text_eol_prompt.replace('<sent>', t)
364
+ sample = format_one_sample(media_file=None, prompt=prompt)
365
+ sample = self.super_processor(sample)
366
+ model_inputs = {}
367
+ for k, v in sample.items():
368
+ if not isinstance(v, torch.Tensor):
369
+ continue
370
+ model_inputs[k] = v.to(self.model.device)
371
+ with torch.inference_mode():
372
+ output = self.model.generate(
373
+ **model_inputs,
374
+ max_new_tokens=1,
375
+ output_hidden_states=True,
376
+ return_dict_in_generate=True,
377
+ )
378
+ emb = output.hidden_states[0][-1][:, -1, :]
379
+ text_embs.append(emb)
380
+ return torch.cat(text_embs)
381
+ else:
382
+ raise ValueError(f"Invalid type for text: {type(text)}")
383
 
384
 
385
  def get_frame_indices(num_frames, vlen, sample='rand', fix_start=None, input_fps=1, max_num_frames=-1):
 
458
  del video_reader
459
 
460
 
 
461
  def read_image_decord(image_path):
462
  image = PIL.Image.open(image_path)
463
  image = image.convert('RGB')
 
478
 
479
 
480
  if __name__ == "__main__":
481
+ from termcolor import colored
482
+ import random
483
+ import numpy as np
484
+ from decord import VideoReader
485
 
486
  # Load model
487
  model = TARA.from_pretrained(
488
+ "/work/piyush/experiments/CaRe/Tarsier2-7b-0115/covr/chiral10k-covr10k/merged_checkpoint/",
489
  device_map='auto',
490
  dtype=torch.bfloat16,
491
  )
 
495
  # Let's encode a sample video
496
  print(colored("Testing video encoding...", 'cyan'))
497
  video_path = "./assets/folding_paper.mp4"
498
+ # video_tensor = read_frames_decord(video_path, num_frames=16)
499
+ # video_tensor = video_tensor.unsqueeze(0)
500
+ # video_tensor = video_tensor.to(model.model.device)
501
  with torch.no_grad():
502
+ video_emb = model.encode_vision(video_path).cpu().squeeze(0).float()
503
+ # print("Video shape:", video_tensor.shape) # torch.Size([1, 16, 3, 240, 426])
504
  print("Video embedding shape:", video_emb.shape) # torch.Size([4096])
505
 
506
  # Let's encode a sample text
preprocessor_config.json CHANGED
@@ -1,9 +1,4 @@
1
  {
2
- "crop_size": {
3
- "height": 336,
4
- "width": 336
5
- },
6
- "do_center_crop": true,
7
  "do_convert_rgb": true,
8
  "do_normalize": true,
9
  "do_rescale": true,
@@ -13,16 +8,22 @@
13
  0.4578275,
14
  0.40821073
15
  ],
16
- "image_processor_type": "CLIPImageProcessor",
17
  "image_std": [
18
  0.26862954,
19
  0.26130258,
20
  0.27577711
21
  ],
22
- "processor_class": "LlavaProcessor",
 
 
 
 
23
  "resample": 3,
24
  "rescale_factor": 0.00392156862745098,
25
  "size": {
26
- "shortest_edge": 336
27
- }
 
 
28
  }
 
1
  {
 
 
 
 
 
2
  "do_convert_rgb": true,
3
  "do_normalize": true,
4
  "do_rescale": true,
 
8
  0.4578275,
9
  0.40821073
10
  ],
11
+ "image_processor_type": "Qwen2VLImageProcessor",
12
  "image_std": [
13
  0.26862954,
14
  0.26130258,
15
  0.27577711
16
  ],
17
+ "max_pixels": 2073600,
18
+ "merge_size": 2,
19
+ "min_pixels": 3136,
20
+ "patch_size": 14,
21
+ "processor_class": "TarsierProcessor",
22
  "resample": 3,
23
  "rescale_factor": 0.00392156862745098,
24
  "size": {
25
+ "max_pixels": 2073600,
26
+ "min_pixels": 3136
27
+ },
28
+ "temporal_patch_size": 2
29
  }
processor_config.json CHANGED
@@ -1,6 +1,8 @@
1
  {
2
- "image_token": "<image>",
3
- "patch_size": null,
4
- "processor_class": "LlavaProcessor",
5
- "vision_feature_select_strategy": null
 
 
6
  }
 
1
  {
2
+ "image_token": "<|image_pad|>",
3
+ "max_seq_len": 16384,
4
+ "merge_size": 2,
5
+ "patch_size": 14,
6
+ "processor_class": "TarsierProcessor",
7
+ "temporal_patch_size": 2
8
  }
shared/__init__.py ADDED
File without changes
shared/run/cut_clips_ego4d.sh ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ video_dir=/scratch/shared/beegfs/shared-datasets/EGO4D/ego4d_data_v1/full_scale/
2
+ cut_dir=/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap/cut_full_scale/
3
+ # csv=/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap/ego4d_chiral_subset-v1-with_reverse_captions-650K.csv
4
+ # csv="/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap/ego4d_chiral_subset-v1-with_reverse_captions-560K_>=0.5s_buffer=0.2.csv"
5
+ # csv="/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap/ego4d_chiral_subset-v1-with_reverse_captions-490K_>=0.5s_buffer=0.2.csv"
6
+ csv="/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap/metadata/cleaned_chiral+general-850K-2025-07-17_18:21:38.csv"
7
+ si=$1
8
+ ei=$2
9
+
10
+ echo "CSV: $csv"
11
+ echo "Start index: $si"
12
+ echo "End index: $ei"
13
+ echo "--------------------------------"
14
+
15
+ # First, cut the original videos into clips based on the CSV
16
+ echo "Cutting clips..."
17
+ python shared/scripts/cut_clips_fast.py \
18
+ --csv $csv \
19
+ --video_dir $video_dir \
20
+ --cut_dir $cut_dir \
21
+ --video_id_key video_id \
22
+ --start_time_key start_sec \
23
+ --end_time_key stop_sec \
24
+ --si $si \
25
+ --ei $ei
26
+
27
+ echo "Done cutting clips."
28
+ echo "--------------------------------"
29
+
30
+ # # Then, downsize the videos to 360 width
31
+ # echo "Downsizing videos..."
32
+ # python shared/scripts/downsize_videos_simple.py --video_dir $cut_dir --remove_old --width 360
33
+ # echo "Done downsizing videos."
34
+ # echo "--------------------------------"
35
+
shared/run/extract_feat_dinov2_ego4d.sh ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DATA_DIR="/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap"
2
+ # csv="${DATA_DIR}/ego4d_chiral_subset-v1-with_reverse_captions-560K_>=0.5s_buffer=0.2.csv"
3
+ # video_dir="${DATA_DIR}/cut_full_scale"
4
+ dataset='ego4d_subset'
5
+ si=$1
6
+ ei=$2
7
+
8
+ python adapt4change/scripts/compute_dino_features.py \
9
+ --dataset $dataset \
10
+ --no_filter_chiral \
11
+ --si $si \
12
+ --ei $ei
shared/run/extract_feat_pe_ego4d.sh ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # csv="/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap/metadata/cleaned_chiral_subset_with_reverse_captions-425K-2025-07-17_18:21:38.csv"
2
+ # csv="/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap/metadata/cleaned_chiral+general-850K-2025-07-17_18:21:38.csv"
3
+ csv="/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap/metadata/cleaned_chiral+general-816K-2025-07-17_18:21:38.csv"
4
+ output_dir="/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap/features/"
5
+ text_col="caption_forward"
6
+ id_col="id"
7
+ si=$1
8
+ ei=$2
9
+
10
+ python chiral_retrieval/scripts/compute_video_text_features.py \
11
+ --csv $csv \
12
+ --output_dir $output_dir \
13
+ --text_col $text_col \
14
+ --id_col $id_col \
15
+ --si $si \
16
+ --ei $ei \
17
+ --devices 1
shared/run/extract_feat_pe_ego4d_reverse.sh ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ csv="/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap/metadata/cleaned_chiral_subset_with_reverse_captions-425K-2025-07-17_18:21:38.csv"
2
+ output_dir="/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap/features/"
3
+ text_col="caption_reverse"
4
+ id_col="id"
5
+ si=$1
6
+ ei=$2
7
+
8
+ python chiral_retrieval/scripts/compute_video_text_features.py \
9
+ --csv $csv \
10
+ --output_dir $output_dir \
11
+ --text_col $text_col \
12
+ --id_col $id_col \
13
+ --si $si \
14
+ --ei $ei \
15
+ --devices 1 \
16
+ --reverse
shared/run/generate_water.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from diffusers import AudioLDM2Pipeline
2
+ import torch
3
+ import scipy.io.wavfile as wavfile
4
+
5
+ def main():
6
+ model_id = "cvssp/audioldm2"
7
+ device = "cuda" if torch.cuda.is_available() else "cpu"
8
+
9
+ # Load the pipeline with half-precision (optional if on GPU)
10
+ pipe = AudioLDM2Pipeline.from_pretrained(model_id, torch_dtype=torch.float16 if device=="cuda" else torch.float32)
11
+ pipe = pipe.to(device)
12
+
13
+ prompt = "High-quality sound of water being poured into a glass in a quiet room"
14
+ audio_length = 5.0 # seconds
15
+ steps = 200
16
+
17
+ outputs = pipe(prompt, num_inference_steps=steps, audio_length_in_s=audio_length)
18
+ audio = outputs.audios[0]
19
+
20
+ wavfile.write("pouring_water.wav", rate=16000, data=audio)
21
+ print("Saved to pouring_water.wav")
22
+
23
+ if __name__ == "__main__":
24
+ main()
shared/run/generate_water_v2.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torchaudio
3
+ from einops import rearrange
4
+ from stable_audio_tools import get_pretrained_model
5
+ from stable_audio_tools.inference.generation import generate_diffusion_cond
6
+
7
+ device = "cuda" if torch.cuda.is_available() else "cpu"
8
+
9
+ # Download model
10
+ model, model_config = get_pretrained_model("stabilityai/stable-audio-open-1.0")
11
+ sample_rate = model_config["sample_rate"]
12
+ sample_size = model_config["sample_size"]
13
+
14
+ model = model.to(device)
15
+
16
+ # Set up text and timing conditioning
17
+ conditioning = [{
18
+ "prompt": "The sound of a glass being filled with hot boiling water.",
19
+ "seconds_start": 0,
20
+ "seconds_total": 10,
21
+ }]
22
+
23
+ # Generate stereo audio
24
+ output = generate_diffusion_cond(
25
+ model,
26
+ steps=100,
27
+ cfg_scale=7,
28
+ conditioning=conditioning,
29
+ sample_size=sample_size,
30
+ sigma_min=0.3,
31
+ sigma_max=500,
32
+ sampler_type="dpmpp-3m-sde",
33
+ device=device
34
+ )
35
+
36
+ # Rearrange audio batch to a single sequence
37
+ output = rearrange(output, "b d n -> d (b n)")
38
+
39
+ # Peak normalize, clip, convert to int16, and save to file
40
+ output = output.to(torch.float32).div(torch.max(torch.abs(output))).clamp(-1, 1).mul(32767).to(torch.int16).cpu()
41
+ torchaudio.save("pouring_water_hot.wav", output, sample_rate)
shared/run/run.sh ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ CUDA_VISIBLE_DEVICES=0 bash shared/run/extract_feat_dinov2_ego4d.sh 0 110000 &
2
+ CUDA_VISIBLE_DEVICES=1 bash shared/run/extract_feat_dinov2_ego4d.sh 110000 220000 &
3
+ CUDA_VISIBLE_DEVICES=2 bash shared/run/extract_feat_dinov2_ego4d.sh 220000 330000 &
4
+ CUDA_VISIBLE_DEVICES=3 bash shared/run/extract_feat_dinov2_ego4d.sh 330000 440000 & wait
shared/scripts/avi_to_mp4.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import glob
3
+ import argparse
4
+ from moviepy.editor import VideoFileClip
5
+ import sys
6
+
7
+ def convert_avi_to_mp4(src_dir, dst_dir, ext, start_idx, end_idx):
8
+ # Ensure destination directory exists
9
+ os.makedirs(dst_dir, exist_ok=True)
10
+
11
+ # Get list of all .avi files in subdirectories
12
+ avi_files = glob.glob(os.path.join(src_dir, f'**/*.{ext}'), recursive=True)
13
+
14
+ if not avi_files:
15
+ print(f"No .{ext} files found in {src_dir}")
16
+ return
17
+
18
+ # Apply start and end index filtering
19
+ avi_files = avi_files[start_idx:end_idx]
20
+
21
+ for avi_file in avi_files:
22
+ # Get file ID without extension and parent path
23
+ file_id = os.path.splitext(os.path.relpath(avi_file, src_dir))[0]
24
+ mp4_file = os.path.join(dst_dir, f"{file_id}.mp4")
25
+
26
+ # Create any necessary subdirectories in dst_dir
27
+ os.makedirs(os.path.dirname(mp4_file), exist_ok=True)
28
+
29
+ try:
30
+ # Suppress moviepy verbose output
31
+ sys.stdout = open(os.devnull, 'w')
32
+ clip = VideoFileClip(avi_file)
33
+ clip.write_videofile(mp4_file, codec="libx264", audio_codec="aac", verbose=False, logger=None)
34
+ sys.stdout = sys.__stdout__
35
+ print(f"Converted: {avi_file} -> {mp4_file}")
36
+ except Exception as e:
37
+ sys.stdout = sys.__stdout__
38
+ print(f"Error converting {avi_file}: {e}")
39
+
40
+ if __name__ == "__main__":
41
+ parser = argparse.ArgumentParser(description="Convert .avi files to .mp4")
42
+ parser.add_argument("--src_dir", required=True, help="Source directory containing .avi files")
43
+ parser.add_argument("--dst_dir", required=True, help="Destination directory for .mp4 files")
44
+ parser.add_argument("--ext", default="avi", help="Extension of the source files (default: avi)")
45
+ parser.add_argument("--si", type=int, default=0, help="Start index of files to process (default: 0)")
46
+ parser.add_argument("--ei", type=int, default=None, help="End index of files to process (default: None)")
47
+
48
+ args = parser.parse_args()
49
+
50
+ convert_avi_to_mp4(args.src_dir, args.dst_dir, args.ext, args.si, args.ei)
shared/scripts/check_cut_files.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Checks cut files."""
2
+ import os
3
+ import sys
4
+ from glob import glob
5
+ from tqdm import tqdm
6
+ from joblib import Parallel, delayed
7
+
8
+ import decord
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+
13
+ if __name__ == "__main__":
14
+ video_dir = "/work/piyush/from_nfs2/datasets/EPIC-Kitchens-100/cut_clips"
15
+ files = glob(os.path.join(video_dir, "*/*/*.MP4"))
16
+ print("Total files:", len(files))
17
+
18
+ parallel = True
19
+
20
+ if not parallel:
21
+ failed = []
22
+ iterator = tqdm(files, desc="Checking files")
23
+ for f in iterator:
24
+ try:
25
+ vr = decord.VideoReader(f, ctx=decord.cpu(), num_threads=1)
26
+ random_frame = np.random.randint(0, len(vr))
27
+ random_frame = vr.get_batch([random_frame]).asnumpy()
28
+ except Exception as e:
29
+ failed.append(f)
30
+ import ipdb; ipdb.set_trace()
31
+ else:
32
+ def check_file(f):
33
+ try:
34
+ vr = decord.VideoReader(f, ctx=decord.cpu(), num_threads=1)
35
+ random_frame = np.random.randint(0, len(vr))
36
+ random_frame = len(vr) - 1
37
+ random_frame = vr.get_batch([random_frame]).asnumpy()
38
+ return None
39
+ except Exception as e:
40
+ return f
41
+
42
+ status = Parallel(n_jobs=24)(
43
+ delayed(check_file)(f) for f in tqdm(files, desc="Checking files")
44
+ )
45
+ failed = [f for f in status if f is not None]
46
+ print("Number of files on which loading failed:", len(failed))
47
+ import ipdb; ipdb.set_trace()
48
+
49
+ for f in failed: os.remove(f)
50
+
shared/scripts/check_video_health.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Checks videos in a CSV for health."""
2
+ import os
3
+ import sys
4
+ from joblib import Parallel, delayed
5
+
6
+ import pandas as pd
7
+ import numpy as np
8
+ from torchcodec.decoders import SimpleVideoDecoder
9
+ import decord
10
+
11
+ import shared.utils as su
12
+
13
+
14
+ def get_video_width(path):
15
+ try:
16
+ return SimpleVideoDecoder(path).metadata.width
17
+ except:
18
+ return -1
19
+
20
+
21
+ def check_decord_videoreader(path):
22
+ try:
23
+ vr = decord.VideoReader(path)
24
+ return True
25
+ except:
26
+ return False
27
+
28
+
29
+ def check_decord_random_frame(path):
30
+ try:
31
+ vr = decord.VideoReader(path)
32
+ i = np.random.randint(len(vr))
33
+ frame = vr[i]
34
+ return True
35
+ except:
36
+ return False
37
+
38
+
39
+ if __name__ == "__main__":
40
+ # Configure video_dir and csv_path
41
+ data_dir = "/scratch/shared/beegfs/piyush/datasets/Ego4D-HCap"
42
+ video_dir = f"{data_dir}/cut_full_scale"
43
+ csv_path = f"{data_dir}/metadata/"\
44
+ "cleaned_chiral_subset_with_reverse_captions-425K-2025-07-17_18:21:38.csv"
45
+ id_col = "id"
46
+ save_dir = "./outputs/ego4d_video_health"
47
+ os.makedirs(save_dir, exist_ok=True)
48
+
49
+ health_checks = [
50
+ # "video_exists",
51
+ "video_widths",
52
+ # "decord_videoreader",
53
+ # "decord_random_frame",
54
+ ]
55
+
56
+
57
+ assert os.path.isdir(video_dir), f"Video directory {video_dir} does not exist"
58
+ assert os.path.exists(csv_path), f"CSV file {csv_path} does not exist"
59
+
60
+ # Load the CSV
61
+ su.log.print_update(f"Loading CSV: {csv_path}")
62
+ df = pd.read_csv(csv_path)
63
+ print("Number of rows in CSV:", len(df))
64
+
65
+ # Add video_path column
66
+ df['video_path'] = df[id_col].apply(lambda x: f"{video_dir}/{x}.mp4")
67
+
68
+
69
+ # 1. Check if the video files exist
70
+ if "video_exists" in health_checks:
71
+ su.log.print_update("Checking if the video files exist")
72
+ iterator = su.log.tqdm_iterator(df.video_path.tolist())
73
+ video_exists = Parallel(n_jobs=-1)(delayed(os.path.exists)(f) for f in iterator)
74
+ print("Fraction of videos that exist: ", np.mean(video_exists))
75
+ ids_with_missing_videos = df.loc[~np.array(video_exists), id_col].tolist()
76
+ np.save(f"{save_dir}/ids_with_missing_videos.npy", ids_with_missing_videos)
77
+
78
+ # 2. Check if the video widths are valid
79
+ if "video_widths" in health_checks:
80
+ su.log.print_update("Checking if the video widths are valid")
81
+ iterator = su.log.tqdm_iterator(df.video_path.tolist())
82
+ video_widths = Parallel(n_jobs=-1)(delayed(get_video_width)(f) for f in iterator)
83
+ video_widths = np.array(video_widths)
84
+ import ipdb; ipdb.set_trace()
85
+ print("Fraction of videos that are valid: ", np.mean(video_widths != -1))
86
+ ids_with_invalid_widths = df.loc[np.where(video_widths == -1), id_col].tolist()
87
+ np.save(f"{save_dir}/ids_with_invalid_widths.npy", np.array(ids_with_invalid_widths))
88
+
89
+ # 3. Check if the video files are decodable by decord
90
+ if "decord_videoreader" in health_checks:
91
+ su.log.print_update("Checking if the video files are decodable by decord")
92
+ iterator = su.log.tqdm_iterator(df.video_path.tolist())
93
+ decord_videoreader = Parallel(n_jobs=-1)(delayed(check_decord_videoreader)(f) for f in iterator)
94
+ decord_videoreader = np.array(decord_videoreader)
95
+ import ipdb; ipdb.set_trace()
96
+ print("Fraction of videos that are decodable by decord: ", np.mean(decord_videoreader))
97
+ ids_with_invalid_decord = df.loc[~decord_videoreader, id_col].tolist()
98
+ np.save(f"{save_dir}/ids_with_invalid_decord.npy", np.array(ids_with_invalid_decord))
99
+
100
+
101
+ # 4. Check if a random frame can be decoded by decord
102
+ if "decord_random_frame" in health_checks:
103
+ su.log.print_update("Checking if a random frame can be decoded by decord")
104
+ iterator = su.log.tqdm_iterator(df.video_path.tolist())
105
+ decord_random_frame = Parallel(n_jobs=-1)(delayed(check_decord_random_frame)(f) for f in iterator)
106
+ decord_random_frame = np.array(decord_random_frame)
107
+ print("Fraction of videos that have a random frame that can be decoded by decord: ", np.mean(decord_random_frame))
108
+ ids_with_invalid_decord_random_frame = df.loc[~decord_random_frame, id_col].tolist()
109
+ np.save(f"{save_dir}/ids_with_invalid_decord_random_frame.npy", np.array(ids_with_invalid_decord_random_frame))
shared/scripts/check_webdataset.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Loads tar files using webdataset."""
2
+ import os
3
+ import webdataset as wds
4
+ import decord
5
+ from torch.utils.data import DataLoader
6
+ import numpy as np
7
+ import einops
8
+
9
+
10
+ # Define a function to decode videos using Decord
11
+ def decode_video(video_bytes):
12
+ # Save the video bytes to a temporary file and decode with decord
13
+ vr = decord.VideoReader(video_bytes)
14
+ frames = [vr[i].asnumpy() for i in range(0, len(vr), 5)] # Frame skip example
15
+ return frames
16
+
17
+
18
+ def convert_bytes_to_frames(video_bytes):
19
+ vr = decord.VideoReader(video_bytes)
20
+ frames = [vr[i].asnumpy() for i in range(0, len(vr), 5)] # Frame skip example
21
+ return frames
22
+
23
+
24
+
25
+ def decode_video(video_bytes):
26
+ """
27
+ Given video bytes, decode them into frames.
28
+ """
29
+ pass
30
+
31
+
32
+ if __name__ == "__main__":
33
+ shard_folder = "/work/piyush/from_nfs2/datasets/SSv2/ssv2_shards/"
34
+ shard_path = os.path.join(shard_folder, "shard-0000.tar")
35
+
36
+ # Define your WebDataset path pattern (all shards)
37
+ dataset_path = os.path.join(shard_folder, "shard-{0000..0002}.tar")
38
+
39
+ ds = wds.WebDataset(dataset_path)
40
+ sample = next(iter(ds))
41
+ dl = DataLoader(ds, batch_size=16, num_workers=8)
42
+ batch = next(iter(dl))
43
+
44
+ # Create a WebDataset loader
45
+ dataset = (
46
+ wds.WebDataset(dataset_path)
47
+ # .decode("rgb") # Ensure that we decode the video bytes into RGB images
48
+ .to_tuple("webm") # Ensure that we get the video bytes and metadata
49
+ # .map_tuple(decode_video) # Apply your video decoding function
50
+ )
51
+ # dataloader = DataLoader(dataset, batch_size=16, num_workers=8)
52
+ # batch = next(iter(dataloader))
53
+ # print(batch[0].shape) # (16, 32, 256, 256, 3) for example
54
+ # H, W, C = 256, 256, 3
55
+ H, W, C = (240, 427, 3)
56
+ for (video,) in dataset:
57
+ # Get file name
58
+
59
+
60
+ # print(len(video[0]))
61
+ print(type(video))
62
+ video_array = np.frombuffer(video, dtype=np.uint8)
63
+ reshaped_video = einops.rearrange(video_array, "(t h w c) -> t c h w", h=H, w=W, c=C)
64
+ # print(len(video))
65
+ # print(video[1])
66
+
67
+ # np_video_bytes = np.frombuffer(video[0], np.uint8)
68
+ break
69
+ import ipdb; ipdb.set_trace()
shared/scripts/convert_frames_to_videos.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Converts frames to videos.
3
+
4
+ input_dir=/scratch/shared/beegfs/piyush/datasets/Jester/images/
5
+ output_dir=/scratch/shared/beegfs/piyush/datasets/Jester/videos/
6
+ python shared/scripts/convert_frames_to_videos.py --input_dir $input_dir --output_dir $output_dir
7
+ """
8
+ import os
9
+ import sys
10
+ import decord
11
+ import moviepy.editor as mpy
12
+ from glob import glob
13
+ from moviepy.editor import ImageSequenceClip
14
+
15
+ import shared.utils as su
16
+
17
+
18
+ def create_video(image_paths, output_file, fps=12):
19
+ """
20
+ Convert a list of image files into an MP4 video
21
+
22
+ Parameters:
23
+ - image_paths: List of image file paths (sorted in desired order)
24
+ - output_file: Output filename (e.g., 'output.mp4')
25
+ - fps: Frames per second (default 12)
26
+ """
27
+ clip = ImageSequenceClip(image_paths, fps=fps, load_images=True)
28
+ clip.write_videofile(output_file, codec='libx264', logger=None)
29
+
30
+
31
+
32
+ if __name__ == "__main__":
33
+ # Read arguments
34
+ import argparse
35
+ parser = argparse.ArgumentParser()
36
+ parser.add_argument("--input_dir", type=str, required=True)
37
+ parser.add_argument("--output_dir", type=str, required=True)
38
+ parser.add_argument("--fps", type=int, default=12)
39
+ parser.add_argument("--extension", type=str, default="jpg")
40
+ parser.add_argument("--debug", action="store_true")
41
+ parser.add_argument("--overwrite", action="store_true")
42
+ args = parser.parse_args()
43
+
44
+ # Create output directory
45
+ os.makedirs(args.output_dir, exist_ok=True)
46
+
47
+ # Get list of all frame folders
48
+ print("Getting list of all frame folders...")
49
+ folders = os.listdir(args.input_dir)
50
+
51
+ # Run the process for each folder
52
+ iterator = su.log.tqdm_iterator(folders, desc="Converting frames to videos")
53
+ for folder in iterator:
54
+
55
+ # Get output path
56
+ save_path = os.path.join(args.output_dir, f"{folder}.mp4")
57
+ if os.path.exists(save_path) and not args.overwrite:
58
+ continue
59
+
60
+ # Get list of frames
61
+ frame_paths = glob(
62
+ os.path.join(args.input_dir, folder, f"*.{args.extension}"),
63
+ )
64
+
65
+ # Save it as a video
66
+ create_video(frame_paths, save_path, fps=args.fps)
67
+
68
+ if args.debug:
69
+ print("Debugging mode. Exiting after processing one folder.")
70
+ break
shared/scripts/convert_webm_to_mp4.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Script to conver webm to mp4
3
+
4
+ Example:
5
+ video_dir=/scratch/shared/beegfs/shared-datasets/SomethingSomething-V2/videos/
6
+ out_dir=/scratch/shared/nfs2/piyush/datasets/SSv2/videos/
7
+ python shared/scripts/convert_webm_to_mp4.py --video_dir $video_dir --out_dir $out_dir
8
+ """
9
+
10
+ import os
11
+ from subprocess import call
12
+ from glob import glob
13
+ from tqdm import tqdm
14
+
15
+
16
+ if __name__ == "__main__":
17
+ import argparse
18
+ parser = argparse.ArgumentParser()
19
+ parser.add_argument("--video_dir", type=str, required=True)
20
+ parser.add_argument("--out_dir", type=str, required=True)
21
+ args = parser.parse_args()
22
+
23
+ ext = ".webm"
24
+
25
+ in_files = glob(os.path.join(args.video_dir, f"*/*{ext}"))
26
+ out_files = [f.replace(f"{ext}", ".mp4") for f in in_files]
27
+ out_files = [f.replace(args.video_dir, args.out_dir) for f in out_files]
28
+
29
+ print("> Set to convert", len(in_files), f"files from {ext} to mp4.")
30
+ iterator = tqdm(range(len(in_files)), desc="Converting videos")
31
+ for i in iterator:
32
+ in_file = in_files[i]
33
+ out_file = out_files[i]
34
+ os.makedirs(os.path.dirname(out_file), exist_ok=True)
35
+
36
+ if not os.path.exists(out_file):
37
+ command = f"ffmpeg -i {in_file} -c:v copy -c:a copy -strict -2 {out_file} -loglevel quiet"
38
+ call(command, shell=True)
39
+ else:
40
+ print(f"Skipping {in_file} as {out_file} already exists.")
41
+
42
+ # os.remove(in_file)
43
+ break
44
+ print("> Done converting.")
shared/scripts/create_webdataset.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import csv
3
+ import argparse
4
+ import multiprocessing as mp
5
+ from pathlib import Path
6
+ from typing import List, Dict
7
+ from functools import partial
8
+ import webdataset as wds
9
+ import torch
10
+ import numpy as np
11
+ from tqdm import tqdm
12
+ from decord import VideoReader
13
+
14
+
15
+ def parse_args():
16
+ parser = argparse.ArgumentParser(description="Convert CSV file to WebDataset format with video data")
17
+ parser.add_argument("--csv_path", type=str, required=True, help="Path to the CSV file")
18
+ parser.add_argument("--output_dir", type=str, required=True, help="Output directory for WebDataset shards")
19
+ parser.add_argument("--num_shards", type=int, default=128, help="Number of shards to create")
20
+ parser.add_argument("--samples_per_shard", type=int, default=None,
21
+ help="Max samples per shard (overrides num_shards if specified)")
22
+ parser.add_argument("--worker_count", type=int, default=mp.cpu_count(),
23
+ help="Number of worker processes")
24
+ parser.add_argument("--shard_prefix", type=str, default="shard",
25
+ help="Prefix for shard filenames")
26
+ parser.add_argument("--video_extension", type=str, default=".webm",
27
+ help="Extension of video files (default: .webm)")
28
+ parser.add_argument("--debug", action="store_true",
29
+ help="Debug mode: create a shard_debug.tar file with max 1000 videos")
30
+ parser.add_argument("--si", type=int, default=0, help="Start index")
31
+ parser.add_argument("--ei", type=int, default=None, help="End index")
32
+ return parser.parse_args()
33
+
34
+
35
+ def read_csv_data(csv_path: str, debug: bool = False) -> List[Dict]:
36
+ """Read the CSV file and return a list of samples."""
37
+ samples = []
38
+ with open(csv_path, 'r') as f:
39
+ reader = csv.DictReader(f)
40
+ for i, row in enumerate(reader):
41
+ samples.append(row)
42
+ # In debug mode, limit to 1000 samples
43
+ if debug and i >= 999:
44
+ break
45
+
46
+ # Select start and end index if specified
47
+ si = args.si
48
+ ei = args.ei if args.ei is not None else len(samples)
49
+ print("Selected samples from index", si, "to", ei)
50
+ samples = samples[si:ei]
51
+
52
+ return samples
53
+
54
+
55
+ def distribute_samples(samples: List[Dict], num_shards: int) -> List[List[Dict]]:
56
+ """Distribute samples across shards."""
57
+ samples_per_shard = len(samples) // num_shards
58
+ remainder = len(samples) % num_shards
59
+
60
+ distributed_samples = []
61
+ start_idx = 0
62
+
63
+ for i in range(num_shards):
64
+ # Add one extra sample for the first 'remainder' shards
65
+ shard_size = samples_per_shard + (1 if i < remainder else 0)
66
+ end_idx = start_idx + shard_size
67
+
68
+ distributed_samples.append(samples[start_idx:end_idx])
69
+ start_idx = end_idx
70
+
71
+ return distributed_samples
72
+
73
+
74
+ def process_shard(shard_samples: List[Dict], shard_path: str, video_extension: str = ".webm"):
75
+ """Process and write a single shard with actual video data."""
76
+ with wds.TarWriter(shard_path) as sink:
77
+ for sample in tqdm(shard_samples, desc=f"Processing {shard_path}"):
78
+ video_path = sample['video_path']
79
+
80
+ vr = VideoReader(video_path, num_threads=1)
81
+ n_frames = len(vr)
82
+ fps = vr.get_avg_fps()
83
+ H, W, _ = vr[0].shape
84
+
85
+ try:
86
+ # Read video file as binary data
87
+ with open(video_path, 'rb') as f:
88
+ video_data = f.read()
89
+
90
+ # Get filename without path for the key
91
+ filename = Path(video_path).stem
92
+
93
+ # Create sample with the actual video data
94
+ sample_dict = {
95
+ "__key__": filename,
96
+ "video": video_data, # Actual video binary data
97
+ "video.extension": video_extension.lstrip('.'), # Store extension without dot
98
+ "target": str(sample['target']), # Target/label
99
+ # "split": str(sample['split']), # Train/val/test split
100
+ "json": dict(n_frames=n_frames, fps=fps, H=H, W=W), # Additional metadata
101
+ }
102
+
103
+ sink.write(sample_dict)
104
+ except Exception as e:
105
+ print(f"Error processing {video_path}: {str(e)}")
106
+
107
+
108
+ import io
109
+ import torchvision
110
+
111
+ def encode_tensor(tensor):
112
+ """
113
+ Convert tensor to bytes in memory.
114
+ """
115
+ # Convert the tensor to bytes in memory
116
+ with io.BytesIO() as buf:
117
+ if isinstance(tensor, torch.Tensor):
118
+ torch.save(tensor, buf) # Save tensor to the buffer
119
+ return buf.getvalue() # Return the byte data
120
+
121
+
122
+ def process_shard_tensor(shard_samples: List[Dict], shard_path: str, video_extension: str = ".webm"):
123
+ """Process and write a single shard with actual video data (actual tensor)."""
124
+ with wds.TarWriter(shard_path) as sink:
125
+ for sample in tqdm(shard_samples, desc=f"Processing {shard_path}"):
126
+ video_path = sample['video_path']
127
+
128
+ # Load the entire video as a tensor
129
+ video, audio, info = torchvision.io.read_video(video_path, pts_unit='sec')
130
+ video_data = encode_tensor(video)
131
+ n_frames = len(video)
132
+ fps = info['video_fps']
133
+ H, W = video.shape[1:-1]
134
+
135
+ try:
136
+ # # Read video file as binary data
137
+ # with open(video_path, 'rb') as f:
138
+ # video_data = f.read()
139
+
140
+ # Get filename without path for the key
141
+ filename = Path(video_path).stem
142
+
143
+ # Create sample with the actual video data
144
+ sample_dict = {
145
+ "__key__": filename,
146
+ "video": video_data, # Actual video binary data
147
+ "video.extension": video_extension.lstrip('.'), # Store extension without dot
148
+ "target": str(sample['target']), # Target/label
149
+ # "split": str(sample['split']), # Train/val/test split
150
+ "json": dict(n_frames=n_frames, fps=fps, H=H, W=W), # Additional metadata
151
+ }
152
+
153
+ sink.write(sample_dict)
154
+ except Exception as e:
155
+ print(f"Error processing {video_path}: {str(e)}")
156
+
157
+
158
+ def create_webdataset(csv_path: str, output_dir: str, num_shards: int,
159
+ samples_per_shard: int = None, worker_count: int = None,
160
+ shard_prefix: str = "shard", video_extension: str = ".webm",
161
+ debug: bool = False):
162
+ """Convert CSV to WebDataset format with video data and parallel processing."""
163
+ os.makedirs(output_dir, exist_ok=True)
164
+
165
+ # Read all samples from the CSV
166
+ print(f"Reading samples from {csv_path}...")
167
+ samples = read_csv_data(csv_path, debug=debug)
168
+ total_samples = len(samples)
169
+ print(f"Found {total_samples} samples in the CSV file")
170
+
171
+ # Handle debug mode with a specific shard_debug.tar file
172
+ if debug:
173
+ print("Debug mode enabled: Creating shard_debug.tar with max 1000 videos")
174
+ debug_shard_path = os.path.join(output_dir, "shard_debug.tar")
175
+ # process_shard(samples, debug_shard_path, video_extension)
176
+ process_shard_tensor(samples, debug_shard_path, video_extension)
177
+
178
+ # Calculate and display file size
179
+ file_size = os.path.getsize(debug_shard_path)
180
+ print(f"Created debug shard: {debug_shard_path}")
181
+ print(f"Debug shard size: {file_size / (1024**2):.2f} MB")
182
+
183
+ # Test the debug shard
184
+ test_dataset(output_dir, debug_pattern="shard_debug.tar")
185
+ return
186
+
187
+ # Determine number of shards based on samples_per_shard if provided
188
+ if samples_per_shard is not None:
189
+ num_shards = (total_samples + samples_per_shard - 1) // samples_per_shard
190
+ print(f"Creating {num_shards} shards with max {samples_per_shard} samples per shard")
191
+ else:
192
+ print(f"Creating {num_shards} shards")
193
+
194
+ # Distribute samples across shards
195
+ shard_samples = distribute_samples(samples, num_shards)
196
+
197
+ # Prepare shard paths
198
+ shard_paths = [
199
+ os.path.join(output_dir, f"{shard_prefix}_{i:05d}.tar")
200
+ for i in range(num_shards)
201
+ ]
202
+
203
+ # Use all available cores if worker_count is not specified
204
+ if worker_count is None:
205
+ worker_count = mp.cpu_count()
206
+
207
+ worker_count = min(worker_count, num_shards) # Don't use more workers than shards
208
+
209
+ print(f"Using {worker_count} worker processes")
210
+
211
+ # Process shards in parallel with video extension
212
+ # process_func = partial(process_shard, video_extension=video_extension)
213
+ process_func = partial(process_shard_tensor, video_extension=video_extension)
214
+
215
+ with mp.Pool(worker_count) as pool:
216
+ list(tqdm(
217
+ pool.starmap(process_func, zip(shard_samples, shard_paths)),
218
+ total=num_shards,
219
+ desc="Creating WebDataset shards with video data"
220
+ ))
221
+
222
+ print(f"Successfully created {num_shards} WebDataset shards in {output_dir}")
223
+
224
+ # Calculate and display total dataset size
225
+ total_size = sum(os.path.getsize(path) for path in shard_paths)
226
+ print(f"Total dataset size: {total_size / (1024**2):.2f} MB")
227
+
228
+
229
+ def test_dataset(output_dir: str, shard_prefix: str = "shard", debug_pattern: str = None):
230
+ """Test reading from the created WebDataset."""
231
+ # Find all shard files or use debug pattern
232
+ if debug_pattern:
233
+ shard_pattern = os.path.join(output_dir, debug_pattern)
234
+ else:
235
+ shard_pattern = os.path.join(output_dir, f"{shard_prefix}_*.tar")
236
+
237
+ # Create a dataset
238
+ dataset = wds.WebDataset(shard_pattern)
239
+
240
+ # Display sample info
241
+ print("\nTesting dataset:")
242
+ for i, sample in enumerate(dataset):
243
+ print(f"Sample {i}:")
244
+ for key, value in sample.items():
245
+ if key == "video":
246
+ print(f" {key}: <binary data of length {len(value)}>")
247
+ else:
248
+ print(f" {key}: {value}")
249
+
250
+ if i >= 2: # Just show a few samples
251
+ break
252
+
253
+
254
+ if __name__ == "__main__":
255
+ args = parse_args()
256
+ create_webdataset(
257
+ csv_path=args.csv_path,
258
+ output_dir=args.output_dir,
259
+ num_shards=args.num_shards,
260
+ samples_per_shard=args.samples_per_shard,
261
+ worker_count=args.worker_count,
262
+ shard_prefix=args.shard_prefix,
263
+ video_extension=args.video_extension,
264
+ debug=args.debug
265
+ )
shared/scripts/cut_clips.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cut video clips from downloaded videos.
3
+
4
+ Example.
5
+ D=/work/piyush/from_nfs2/datasets/Charades
6
+ video_dir=$D/Charades_v1_480/
7
+
8
+ EPIC
9
+
10
+ S=/datasets/EpicKitchens-100/
11
+ D=/work/piyush/from_nfs2/datasets/EPIC-Kitchens-100/cut_clips
12
+ csv=$D/../epic-kitchens-100-annotations/EPIC_100_train_with_id.csv
13
+ python shared/scripts/cut_clips.py --csv $csv --video_id_key path_id --start_time_key start_sec --end_time_key stop_sec --video_dir $S/ --cut_dir $D/ --ext MP4
14
+ """
15
+ import os
16
+ from os.path import join, exists
17
+ from subprocess import call
18
+ import time
19
+
20
+ import numpy as np
21
+ import pandas as pd
22
+ from tqdm import tqdm
23
+
24
+ import shared.utils.io as io
25
+ import shared.utils.log as log
26
+ from video_language.datasets.charades import get_paths, load_main_csv
27
+
28
+
29
+ def time_float_to_str(time_in_seconds):
30
+ import datetime
31
+
32
+ # Calculate hours, minutes, seconds, and milliseconds
33
+ hours, remainder = divmod(time_in_seconds, 3600)
34
+ minutes, seconds_with_ms = divmod(remainder, 60)
35
+ seconds, milliseconds = divmod(int(seconds_with_ms * 1000), 1000)
36
+
37
+ # Create a timedelta object
38
+ time_delta = datetime.timedelta(hours=hours, minutes=minutes, seconds=seconds, milliseconds=milliseconds)
39
+
40
+ # Format the time as HH:MM:SS.mmm
41
+ formatted_time = str(time_delta)
42
+
43
+ return formatted_time
44
+
45
+
46
+ if __name__ == "__main__":
47
+
48
+ import argparse
49
+ parser = argparse.ArgumentParser()
50
+ parser.add_argument(
51
+ "--csv", type=str, required=True,
52
+ help="Path to CSV file containing video IDs and timestamps",
53
+ )
54
+ parser.add_argument(
55
+ "--video_id_key", type=str, default="video_id",
56
+ )
57
+ parser.add_argument(
58
+ "--start_time_key", type=str, default="start_time",
59
+ )
60
+ parser.add_argument(
61
+ "--end_time_key", type=str, default="end_time",
62
+ )
63
+ parser.add_argument(
64
+ "--video_dir", type=str, required=True,
65
+ help="Path to directory containing downloaded videos",
66
+ )
67
+ parser.add_argument(
68
+ "--cut_dir", type=str, required=True,
69
+ help="Path to directory where cut videos will be saved",
70
+ )
71
+ parser.add_argument(
72
+ "--overwrite", action="store_true",
73
+ help="Whether to overwrite existing cut videos",
74
+ )
75
+ parser.add_argument(
76
+ "--verbose", action="store_true",
77
+ )
78
+ parser.add_argument(
79
+ "--no_round_times", action="store_true",
80
+ help="Whether to round start and end times to nearest second in filenames",
81
+ )
82
+ parser.add_argument(
83
+ "--debug", action="store_true",
84
+ )
85
+ parser.add_argument(
86
+ "--ext", type=str, default="mp4",
87
+ )
88
+ parser.add_argument(
89
+ "--si", type=int, default=0,
90
+ )
91
+ parser.add_argument(
92
+ "--ei", type=int, default=None,
93
+ )
94
+ parser.add_argument(
95
+ "--filter_csv", type=str, default=None, required=False,
96
+ )
97
+ parser.add_argument(
98
+ "--filter_key", type=str, default=None, required=False,
99
+ )
100
+ args = parser.parse_args()
101
+
102
+ # Make cut_dir
103
+ os.makedirs(args.cut_dir, exist_ok=True)
104
+
105
+ # Load csv
106
+ assert os.path.exists(args.csv), f"CSV file {args.csv} does not exist."
107
+ df = pd.read_csv(args.csv)
108
+ print(">>> Loaded CSV file with shape", df.shape)
109
+ assert {args.video_id_key, args.start_time_key, args.end_time_key}.issubset(df.columns), \
110
+ f"CSV file must contain columns {args.video_id_key}, {args.start_time_key}, and {args.end_time_key}."
111
+
112
+ # Filter CSV
113
+ if args.filter_csv is not None:
114
+ path = args.filter_csv
115
+ assert os.path.exists(path), f"CSV file {path} does not exist."
116
+
117
+ key = args.filter_key
118
+ df_filter = pd.read_csv(path)
119
+ assert key in df_filter.columns, f"CSV file must contain column {key}."
120
+
121
+ # Only keep the rows in df that match on key with df_filter
122
+ keep_values = df_filter[key].unique()
123
+
124
+ df = df[df[key].isin(keep_values)]
125
+ print(">>> Filtered CSV file with shape", df.shape)
126
+
127
+ # Filter out videos that don't exist
128
+ df["video_path"] = df[args.video_id_key].apply(
129
+ lambda video_id: join(args.video_dir, f"{video_id}.{args.ext}"),
130
+ )
131
+ df["check_video"] = df["video_path"].apply(exists)
132
+ df = df[df["check_video"]]
133
+ del df["check_video"]
134
+ print(">>> Found videos for", df.shape[0], "rows.")
135
+
136
+ if len(df) == 0:
137
+ print(">>> No videos to cut.")
138
+ exit()
139
+
140
+
141
+ si = args.si
142
+ ei = args.ei if args.ei is not None else len(df)
143
+ df = df.iloc[si:ei]
144
+ print("Start index:", si, "End index:", ei)
145
+
146
+ # Custom filter
147
+ # df = df[df.split == "validation"]
148
+ # print(">>> Filtered videos for", df.shape[0], "rows.")
149
+
150
+ if args.debug:
151
+ args.verbose = True
152
+
153
+
154
+ # Cut videos
155
+ ext = args.ext
156
+ iterator = tqdm(range(len(df)), desc="Cutting clips")
157
+ for i in iterator:
158
+
159
+ row = df.iloc[i].to_dict()
160
+ f = row["video_path"]
161
+ v, s, e = row[args.video_id_key], row[args.start_time_key], row[args.end_time_key]
162
+ s = float(s)
163
+ e = float(e)
164
+
165
+ if args.no_round_times:
166
+ clip_filename = f"{v}_{s}_{e}.{ext}"
167
+ else:
168
+ clip_filename = f"{v}_{np.round(s, 1)}_{np.round(e, 1)}.{ext}"
169
+ clip_filepath = join(args.cut_dir, clip_filename)
170
+ os.makedirs(os.path.dirname(clip_filepath), exist_ok=True)
171
+
172
+ if os.path.exists(clip_filepath) and not args.overwrite:
173
+ continue
174
+
175
+ # bring s in HH:MM:SS.mmm format with milliseconds
176
+ s = time_float_to_str(s)
177
+ e = time_float_to_str(e)
178
+ # # bring s in HH:MM:SS. format
179
+ # s = time.strftime("%H:%M:%S", time.gmtime(s))
180
+ # e = time.strftime("%H:%M:%S", time.gmtime(e))
181
+
182
+ # ffmpeg code
183
+ # ffmpeg_source = "/users/piyush/install/ffmpeg-06092024/ffmpeg-7.0.2-i686-static/ffmpeg"
184
+ ffmpeg_source = " /users/piyush/install/ffmpeg/ffmpeg-7.0.2-i686-static/ffmpeg"
185
+ # print("FFMpeg version: ", call(f"{ffmpeg_source} -version", shell=True))
186
+ # use ffmpeg to cut the clip + change spatial resolution to have max height
187
+ # NOTE: also changes spatial resolution to have max width as 480
188
+ command = f"{ffmpeg_source} -i {f} -ss {s} -to {e} -strict -2 -c:v libx264 "\
189
+ f"-pix_fmt yuv420p -c:a copy"\
190
+ " -vf 'scale=480:-1' "\
191
+ f"{clip_filepath} "\
192
+ f"-y -format {ext}"
193
+ if not args.verbose:
194
+ command += " -loglevel quiet"
195
+ else:
196
+ print(">>> Cutting clip", clip_filepath)
197
+ call(command, shell=True)
198
+
199
+ if args.debug:
200
+ print(command)
201
+ break
202
+
203
+ print(">>> Number of cut files:", len(os.listdir(args.cut_dir)))
shared/scripts/cut_clips_fast.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Faster clip cutting script generated by Claude.
3
+
4
+ S=/datasets/EpicKitchens-100/
5
+ D=/work/piyush/from_nfs2/datasets/EPIC-Kitchens-100/cut_clips
6
+ csv=$D/../epic-kitchens-100-annotations/EPIC_100_train_with_id.csv
7
+ python shared/scripts/cut_clips_fast.py --csv $csv --video_id_key path_id --start_time_key start_sec --end_time_key stop_sec --video_dir $S/ --cut_dir $D/ --ext MP4 --max_workers 4
8
+
9
+ """
10
+ import os
11
+ from os.path import join, exists
12
+ import time
13
+ from concurrent.futures import ThreadPoolExecutor, as_completed
14
+
15
+ import numpy as np
16
+ import pandas as pd
17
+ from tqdm import tqdm
18
+ from moviepy.editor import VideoFileClip
19
+ from moviepy.video.fx.resize import resize
20
+
21
+ def time_float_to_str(time_in_seconds):
22
+ import datetime
23
+ hours, remainder = divmod(time_in_seconds, 3600)
24
+ minutes, seconds_with_ms = divmod(remainder, 60)
25
+ seconds, milliseconds = divmod(int(seconds_with_ms * 1000), 1000)
26
+ time_delta = datetime.timedelta(hours=hours, minutes=minutes, seconds=seconds, milliseconds=milliseconds)
27
+ return str(time_delta)
28
+
29
+ def process_video(row, args):
30
+ """Process a single video clip"""
31
+ try:
32
+ f = row["video_path"]
33
+ v, s, e = row[args.video_id_key], float(row[args.start_time_key]), float(row[args.end_time_key])
34
+
35
+ if args.no_round_times:
36
+ clip_filename = f"{v}_{s}_{e}.{args.ext}"
37
+ else:
38
+ clip_filename = f"{v}_{np.round(s, 1)}_{np.round(e, 1)}.{args.ext}"
39
+
40
+ clip_filepath = join(args.cut_dir, clip_filename)
41
+ os.makedirs(os.path.dirname(clip_filepath), exist_ok=True)
42
+
43
+ if os.path.exists(clip_filepath) and not args.overwrite:
44
+ return None
45
+
46
+ # Load video and extract clip
47
+ with VideoFileClip(f) as video:
48
+ # Calculate target width maintaining aspect ratio with max height 480
49
+ aspect_ratio = video.w / video.h
50
+ target_height = 480
51
+ target_width = int(target_height * aspect_ratio)
52
+
53
+ # Extract and resize clip
54
+ clip = video.subclip(s, e)
55
+ clip = clip.resize(width=target_width, height=target_height)
56
+
57
+ # Write clip with optimized settings
58
+ clip.write_videofile(
59
+ clip_filepath,
60
+ codec='libx264',
61
+ audio_codec='aac',
62
+ preset='faster', # Faster encoding
63
+ threads=2, # Use multiple threads for encoding
64
+ logger=None if not args.verbose else None
65
+ )
66
+
67
+ return clip_filepath
68
+ except Exception as e:
69
+ if args.verbose:
70
+ print(f"Error processing {row[args.video_id_key]}: {str(e)}")
71
+ return None
72
+
73
+ if __name__ == "__main__":
74
+ import argparse
75
+ parser = argparse.ArgumentParser()
76
+ parser.add_argument(
77
+ "--csv", type=str, required=True,
78
+ help="Path to CSV file containing video IDs and timestamps",
79
+ )
80
+ parser.add_argument(
81
+ "--video_id_key", type=str, default="video_id",
82
+ )
83
+ parser.add_argument(
84
+ "--start_time_key", type=str, default="start_time",
85
+ )
86
+ parser.add_argument(
87
+ "--end_time_key", type=str, default="end_time",
88
+ )
89
+ parser.add_argument(
90
+ "--video_dir", type=str, required=True,
91
+ help="Path to directory containing downloaded videos",
92
+ )
93
+ parser.add_argument(
94
+ "--cut_dir", type=str, required=True,
95
+ help="Path to directory where cut videos will be saved",
96
+ )
97
+ parser.add_argument(
98
+ "--overwrite", action="store_true",
99
+ help="Whether to overwrite existing cut videos",
100
+ )
101
+ parser.add_argument(
102
+ "--verbose", action="store_true",
103
+ )
104
+ parser.add_argument(
105
+ "--no_round_times", action="store_true",
106
+ help="Whether to round start and end times to nearest second in filenames",
107
+ )
108
+ parser.add_argument(
109
+ "--debug", action="store_true",
110
+ )
111
+ parser.add_argument(
112
+ "--ext", type=str, default="mp4",
113
+ )
114
+ parser.add_argument(
115
+ "--si", type=int, default=0,
116
+ )
117
+ parser.add_argument(
118
+ "--ei", type=int, default=None,
119
+ )
120
+ parser.add_argument(
121
+ "--filter_csv", type=str, default=None, required=False,
122
+ )
123
+ parser.add_argument(
124
+ "--filter_key", type=str, default=None, required=False,
125
+ )
126
+ parser.add_argument(
127
+ "--max_workers", type=int, default=4,
128
+ help="Number of parallel workers for processing videos",
129
+ )
130
+ args = parser.parse_args()
131
+
132
+ # Make cut_dir
133
+ os.makedirs(args.cut_dir, exist_ok=True)
134
+
135
+ # Load and filter CSV
136
+ assert os.path.exists(args.csv), f"CSV file {args.csv} does not exist."
137
+ df = pd.read_csv(args.csv)
138
+ print(">>> Loaded CSV file with shape", df.shape)
139
+ assert {args.video_id_key, args.start_time_key, args.end_time_key}.issubset(df.columns)
140
+
141
+ # Filter CSV if needed
142
+ if args.filter_csv is not None:
143
+ path = args.filter_csv
144
+ assert os.path.exists(path), f"CSV file {path} does not exist."
145
+ key = args.filter_key
146
+ df_filter = pd.read_csv(path)
147
+ assert key in df_filter.columns, f"CSV file must contain column {key}."
148
+ keep_values = df_filter[key].unique()
149
+ df = df[df[key].isin(keep_values)]
150
+ print(">>> Filtered CSV file with shape", df.shape)
151
+
152
+ # Apply index slicing
153
+ si = args.si
154
+ ei = args.ei if args.ei is not None else len(df)
155
+ df = df.iloc[si:ei]
156
+ print("Start index:", si, "End index:", ei)
157
+
158
+ # More efficient way to add video path
159
+ print(">>> Adding video paths to dataframe")
160
+ video_ids = df[args.video_id_key].unique()
161
+ video_paths = [join(args.video_dir, f"{video_id}.{args.ext}") for video_id in video_ids]
162
+ video_id_to_path = {video_id: path for video_id, path in zip(video_ids, video_paths)}
163
+ df["video_path"] = df[args.video_id_key].map(video_id_to_path)
164
+ # df = df[df["video_path"].apply(exists)]
165
+ df['check_video'] = df['video_path'].apply(exists)
166
+ df = df[df['check_video']]
167
+ del df['check_video']
168
+ print(">>> Found videos for", df.shape[0], "rows.")
169
+
170
+ # # Filter out videos that don't exist
171
+ # df["video_path"] = df[args.video_id_key].apply(
172
+ # lambda video_id: join(args.video_dir, f"{video_id}.{args.ext}"),
173
+ # )
174
+ # df["check_video"] = df["video_path"].apply(exists)
175
+ # df = df[df["check_video"]]
176
+ # del df["check_video"]
177
+ # print(">>> Found videos for", df.shape[0], "rows.")
178
+
179
+ if len(df) == 0:
180
+ print(">>> No videos to cut.")
181
+ exit()
182
+
183
+
184
+ if args.debug:
185
+ args.verbose = True
186
+ # Process only one video in debug mode
187
+ process_video(df.iloc[0], args)
188
+ else:
189
+ # Process videos in parallel
190
+ with ThreadPoolExecutor(max_workers=args.max_workers) as executor:
191
+ futures = [executor.submit(process_video, row, args)
192
+ for _, row in df.iterrows()]
193
+
194
+ # Show progress bar
195
+ with tqdm(total=len(futures), desc="Cutting clips") as pbar:
196
+ for future in as_completed(futures):
197
+ result = future.result()
198
+ pbar.update(1)
199
+
200
+ print(">>> Number of cut files:", len(os.listdir(args.cut_dir)))
shared/scripts/cut_multiple_clips.py ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cuts multiple clips from a single video using ffmpeg."""
2
+ import os
3
+ from os.path import join, exists
4
+ import numpy as np
5
+ import pandas as pd
6
+ from subprocess import call
7
+ from tqdm import tqdm
8
+
9
+
10
+ def cut_multiple_clips_video_only(
11
+ video_path: str,
12
+ start_times: list,
13
+ end_times: list,
14
+ save_dir: str,
15
+ video_id=None,
16
+ ext=None,
17
+ verbose=False,
18
+ ):
19
+ """Cuts multiple clips from a single video using ffmpeg.
20
+
21
+ Args:
22
+ video_path: Path to the video file.
23
+ start_times: List of start times for each clip.
24
+ end_times: List of end times for each clip.
25
+ """
26
+ os.makedirs(save_dir, exist_ok=True)
27
+ assert len(start_times) == len(end_times), \
28
+ 'start_times and end_times must have the same length.'
29
+
30
+ if video_id is None:
31
+ video_id = os.path.basename(video_path).split(".")[0]
32
+ if ext is None:
33
+ ext = os.path.basename(video_path).split(".")[1]
34
+
35
+ item_ids = [
36
+ f"{video_id}_{np.round(s, 1)}_{np.round(e, 1)}" \
37
+ for (s, e) in zip(start_times, end_times)
38
+ ]
39
+
40
+ ins = [
41
+ f"[0:v]trim=start={s}:end={e},setpts=PTS-STARTPTS,scale=480:-1[v{i}]" \
42
+ for i, (s, e) in enumerate(zip(start_times, end_times))
43
+ ]
44
+ ins = ";".join(ins)
45
+ outs = [
46
+ f"-map [v{i}] {save_dir}/{item_ids[i]}.{ext}" \
47
+ for i in range(len(start_times))
48
+ ]
49
+ outs = " ".join(outs)
50
+ if not verbose:
51
+ suffix = "-loglevel panic"
52
+ else:
53
+ suffix = ""
54
+ command = f"""
55
+ ffmpeg -i {video_path} -filter_complex "{ins}" {outs} -y {suffix}
56
+ """
57
+ call(command, shell=True)
58
+
59
+
60
+ def cut_multiple_clips_audio_and_video(
61
+ video_path: str,
62
+ start_times: list,
63
+ end_times: list,
64
+ save_dir: str,
65
+ video_id=None,
66
+ ext=None,
67
+ verbose=False,
68
+ ):
69
+ """Cuts multiple clips from a single video using ffmpeg.
70
+
71
+ Args:
72
+ video_path: Path to the video file.
73
+ start_times: List of start times for each clip.
74
+ end_times: List of end times for each clip.
75
+ """
76
+
77
+ if args.verbose:
78
+ print("[:::] Cutting clips from video: ", video_path)
79
+ print("[:::] Number of clips to cut: ", len(start_times))
80
+
81
+ os.makedirs(save_dir, exist_ok=True)
82
+ assert len(start_times) == len(end_times), \
83
+ 'start_times and end_times must have the same length.'
84
+
85
+ if video_id is None:
86
+ video_id = os.path.basename(video_path).split(".")[0]
87
+ if ext is None:
88
+ ext = os.path.basename(video_path).split(".")[1]
89
+
90
+ item_ids = [
91
+ f"{video_id}_{np.round(s, 1)}_{np.round(e, 1)}" \
92
+ for (s, e) in zip(start_times, end_times)
93
+ ]
94
+
95
+ ins = [
96
+ f"[0:v]trim=start={s}:end={e},setpts=PTS-STARTPTS,scale=480:-1[v{i}];"\
97
+ f"[0:a:0]atrim=start={s}:end={e},asetpts=PTS-STARTPTS[a{i}]" \
98
+ for i, (s, e) in enumerate(zip(start_times, end_times))
99
+ ]
100
+ ins = ";".join(ins)
101
+ outs = [
102
+ f"-map [v{i}] -map [a{i}] {save_dir}/{item_ids[i]}.{ext}" \
103
+ for i in range(len(start_times))
104
+ ]
105
+ outs = " ".join(outs)
106
+ if not verbose:
107
+ suffix = "-loglevel panic"
108
+ else:
109
+ suffix = ""
110
+ command = f"""
111
+ ffmpeg -i {video_path} -filter_complex "{ins}" {outs} -y {suffix}
112
+ """
113
+ call(command, shell=True)
114
+
115
+
116
+
117
+
118
+ def cut_multiple_clips_audio_and_video_v2(
119
+ video_path: str,
120
+ start_times: list,
121
+ end_times: list,
122
+ save_dir: str,
123
+ video_id=None,
124
+ ext=None,
125
+ verbose=False,
126
+ ):
127
+ """Cuts multiple clips from a single video using ffmpeg.
128
+
129
+ Args:
130
+ video_path: Path to the video file.
131
+ start_times: List of start times for each clip.
132
+ end_times: List of end times for each clip.
133
+ """
134
+
135
+ if args.verbose:
136
+ print("[:::] Cutting clips from video: ", video_path)
137
+ print("[:::] Number of clips to cut: ", len(start_times))
138
+
139
+ os.makedirs(save_dir, exist_ok=True)
140
+ assert len(start_times) == len(end_times), \
141
+ 'start_times and end_times must have the same length.'
142
+
143
+ if video_id is None:
144
+ video_id = os.path.basename(video_path).split(".")[0]
145
+ if ext is None:
146
+ ext = os.path.basename(video_path).split(".")[1]
147
+
148
+ item_ids = [
149
+ f"{video_id}_{np.round(s, 1)}_{np.round(e, 1)}" \
150
+ for (s, e) in zip(start_times, end_times)
151
+ ]
152
+
153
+ ins = [
154
+ f"[0:v]trim=start={s}:end={e},setpts=PTS-STARTPTS,scale=480:-1[v{i}];"\
155
+ f"[0:a:0]atrim=start={s}:end={e},asetpts=PTS-STARTPTS[a{i}]" \
156
+ for i, (s, e) in enumerate(zip(start_times, end_times))
157
+ ]
158
+ # ins = ";".join(ins)
159
+ outs = [
160
+ f"-map [v{i}] -map [a{i}] {save_dir}/{item_ids[i]}.{ext}" \
161
+ for i in range(len(start_times))
162
+ ]
163
+ # outs = " ".join(outs)
164
+ if not verbose:
165
+ suffix = "-loglevel panic"
166
+ else:
167
+ suffix = ""
168
+
169
+ iterator = tqdm(range(len(start_times)), desc="Cutting clips for {}".format(video_id))
170
+ for i in iterator:
171
+ ins_ = ins[i]
172
+ outs_ = outs[i]
173
+ save_path = f"{save_dir}/{item_ids[i]}.{ext}"
174
+ os.makedirs(os.path.dirname(save_path), exist_ok=True)
175
+ if os.path.exists(save_path):
176
+ continue
177
+ command = f"""
178
+ ffmpeg -i {video_path} -filter_complex "{ins_}" {outs_} -y {suffix}
179
+ """
180
+ call(command, shell=True)
181
+
182
+
183
+ if __name__ == "__main__":
184
+ import argparse
185
+ parser = argparse.ArgumentParser()
186
+ # General arguments
187
+ parser.add_argument("--sanity", action="store_true")
188
+ parser.add_argument("--debug", action="store_true")
189
+ parser.add_argument("--verbose", action="store_true")
190
+ parser.add_argument(
191
+ "--ext", type=str, default="mp4",
192
+ )
193
+ # Arguments for input CSV
194
+ parser.add_argument(
195
+ "--csv", type=str, required=True,
196
+ help="Path to CSV file containing video IDs and timestamps",
197
+ )
198
+ parser.add_argument(
199
+ "--video_id_key", type=str, default="video_id",
200
+ )
201
+ parser.add_argument(
202
+ "--start_time_key", type=str, default="start_time",
203
+ )
204
+ parser.add_argument(
205
+ "--end_time_key", type=str, default="end_time",
206
+ )
207
+ parser.add_argument(
208
+ "--video_dir", type=str, required=True,
209
+ help="Path to directory containing downloaded videos",
210
+ )
211
+ parser.add_argument(
212
+ "--cut_dir", type=str, required=True,
213
+ help="Path to directory where cut videos will be saved",
214
+ )
215
+ parser.add_argument(
216
+ "--overwrite", action="store_true",
217
+ help="Whether to overwrite existing cut videos",
218
+ )
219
+ parser.add_argument(
220
+ "--video_only", action="store_true",
221
+ )
222
+ parser.add_argument(
223
+ "--si", type=int, default=0,
224
+ )
225
+ parser.add_argument(
226
+ "--ei", type=int, default=None,
227
+ )
228
+ args = parser.parse_args()
229
+
230
+ if args.sanity:
231
+
232
+ # Test without audio
233
+ video_path = "sample_data/folding_paper.mp4"
234
+ start_times = [0, 5, 10]
235
+ end_times = [5, 10, 15]
236
+ cut_multiple_clips_video_only(
237
+ video_path,
238
+ start_times,
239
+ end_times,
240
+ "./sample_data/clips",
241
+ verbose=args.verbose,
242
+ ext=args.ext,
243
+ )
244
+
245
+ # Test with audio
246
+ video_path = "sample_data/pouring_water_youtube.mp4"
247
+ start_times = [0, 5, 10]
248
+ end_times = [5, 10, 15]
249
+ cut_multiple_clips_audio_and_video(
250
+ video_path,
251
+ start_times,
252
+ end_times,
253
+ "./sample_data/clips",
254
+ verbose=args.verbose,
255
+ ext=args.ext,
256
+ )
257
+
258
+ else:
259
+
260
+ # Make cut_dir
261
+ os.makedirs(args.cut_dir, exist_ok=True)
262
+
263
+ # Load csv
264
+ assert os.path.exists(args.csv), f"CSV file {args.csv} does not exist."
265
+ df = pd.read_csv(args.csv)
266
+ print(">>> Loaded CSV file with shape", df.shape)
267
+ keys = [args.video_id_key, args.start_time_key, args.end_time_key]
268
+ assert set(keys).issubset(df.columns), \
269
+ f"CSV file must contain columns {keys}."
270
+
271
+ # Filter out videos that don't exist
272
+ df["video_path"] = df[args.video_id_key].apply(
273
+ lambda video_id: join(args.video_dir, f"{video_id}.{args.ext}"),
274
+ )
275
+ df["check_video"] = df["video_path"].apply(exists)
276
+ df = df[df["check_video"]]
277
+ del df["check_video"]
278
+ print(">>> Found videos for", df.shape[0], "rows.")
279
+
280
+ si = args.si
281
+ ei = args.ei if args.ei is not None else df.shape[0]
282
+ print("Running from indices", si, "to", ei)
283
+ df = df.iloc[si:ei]
284
+
285
+ if args.debug:
286
+ args.verbose = True
287
+ ext = args.ext
288
+
289
+ # Iterate over each video
290
+ video_paths = df["video_path"].unique()
291
+ # iterator = tqdm(range(len(video_paths)), desc="Cutting clips")
292
+ print("Number of unique videos:", len(video_paths))
293
+ for i in range(len(video_paths)):
294
+ video_path = video_paths[i]
295
+
296
+ # Find rows corresponding to this video
297
+ df_video = df[df["video_path"] == video_path]
298
+ print("Number of clips to cut from video", video_path, ":", df_video.shape[0])
299
+ start_times = df_video[args.start_time_key].values
300
+ end_times = df_video[args.end_time_key].values
301
+ video_id = df_video[args.video_id_key].values[0]
302
+
303
+ """
304
+ # Cut to MAXLEN clips per video
305
+ MAX_LEN = 10
306
+ start_times_batches = np.array_split(start_times, MAX_LEN)
307
+ end_times_batches = np.array_split(end_times, MAX_LEN)
308
+ for start_times_, end_times_ in zip(start_times_batches, end_times_batches):
309
+ if args.video_only:
310
+ cut_multiple_clips_video_only(
311
+ video_path,
312
+ start_times_,
313
+ end_times_,
314
+ args.cut_dir,
315
+ video_id=video_id,
316
+ ext=ext,
317
+ verbose=args.verbose,
318
+ # verbose=True,
319
+ )
320
+ else:
321
+ cut_multiple_clips_audio_and_video_v2(
322
+ video_path,
323
+ start_times_,
324
+ end_times_,
325
+ args.cut_dir,
326
+ video_id=video_id,
327
+ ext=ext,
328
+ verbose=args.verbose,
329
+ # verbose=True,
330
+ )
331
+ """
332
+ # """
333
+ # Cut videos
334
+ if args.video_only:
335
+ cut_multiple_clips_video_only(
336
+ video_path,
337
+ start_times,
338
+ end_times,
339
+ args.cut_dir,
340
+ video_id=video_id,
341
+ ext=ext,
342
+ verbose=args.verbose,
343
+ )
344
+ else:
345
+ cut_multiple_clips_audio_and_video_v2(
346
+ video_path,
347
+ start_times,
348
+ end_times,
349
+ args.cut_dir,
350
+ video_id=video_id,
351
+ ext=ext,
352
+ verbose=args.verbose,
353
+ )
354
+ # """
355
+
356
+ if args.debug:
357
+ break
shared/scripts/downscale_videos.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ import cv2
9
+ from tqdm import tqdm
10
+ from joblib import Parallel, delayed
11
+ from contextlib import contextmanager
12
+ from joblib import parallel as joblib_parallel
13
+
14
+
15
+ def parse_args() -> argparse.Namespace:
16
+ parser = argparse.ArgumentParser(
17
+ description="Downscale videos and save them to a new folder with a given extension."
18
+ )
19
+ parser.add_argument(
20
+ "--video_dir",
21
+ type=Path,
22
+ default=Path("/scratch/shared/beegfs/piyush/datasets/NTU/nturgb+d_rgb/"),
23
+ help="Root directory containing videos (searched recursively)",
24
+ )
25
+ parser.add_argument(
26
+ "--ext",
27
+ type=str,
28
+ default="avi",
29
+ help="Video file extension to search for (without dot)",
30
+ )
31
+ parser.add_argument(
32
+ "--downscale_factor",
33
+ type=float,
34
+ default=0.4,
35
+ help="Factor by which to downscale width and height (e.g., 0.4)",
36
+ )
37
+ parser.add_argument(
38
+ "--save_dir",
39
+ type=Path,
40
+ default=None,
41
+ help="Directory to save downscaled videos. Defaults to video_dir + '-downscaled={factor}'",
42
+ )
43
+ parser.add_argument(
44
+ "--save_ext",
45
+ type=str,
46
+ default="mp4",
47
+ help="Extension to save resulting videos with (without dot)",
48
+ )
49
+ parser.add_argument(
50
+ "--debug",
51
+ action="store_true",
52
+ help="Process only the first 10 videos and print saved paths",
53
+ )
54
+ parser.add_argument(
55
+ "--si",
56
+ type=int,
57
+ default=0,
58
+ help="Start index (inclusive) in the sorted video list",
59
+ )
60
+ parser.add_argument(
61
+ "--ei",
62
+ type=int,
63
+ default=None,
64
+ help="End index (exclusive) in the sorted video list; None means till end",
65
+ )
66
+ parser.add_argument(
67
+ "--n_jobs",
68
+ type=int,
69
+ default=-1,
70
+ help="Number of parallel jobs (-1 uses all cores)",
71
+ )
72
+ args = parser.parse_args()
73
+
74
+ if args.save_dir is None:
75
+ args.save_dir = Path(f"{args.video_dir}-downscaled={args.downscale_factor}")
76
+
77
+ # Normalize extensions (strip leading dots)
78
+ args.ext = args.ext.lstrip('.')
79
+ args.save_ext = args.save_ext.lstrip('.')
80
+ return args
81
+
82
+
83
+ def ensure_even_dimension(value: int) -> int:
84
+ if value < 1:
85
+ return 1
86
+ return value if value % 2 == 0 else value - 1 if value > 1 else 1
87
+
88
+
89
+ def list_videos(video_dir: Path, ext: str) -> List[Path]:
90
+ pattern = f"**/*.{ext}"
91
+ return sorted(video_dir.rglob(pattern))
92
+
93
+
94
+ def change_extension(path: Path, new_ext: str) -> Path:
95
+ return path.with_suffix('.' + new_ext)
96
+
97
+
98
+ def downscale_video(
99
+ src_path: Path,
100
+ dst_path: Path,
101
+ downscale_factor: float,
102
+ save_ext: str,
103
+ ) -> bool:
104
+ cap = cv2.VideoCapture(str(src_path))
105
+ if not cap.isOpened():
106
+ return False
107
+
108
+ # Read properties
109
+ fps = cap.get(cv2.CAP_PROP_FPS)
110
+ if fps <= 0 or fps != fps: # NaN check
111
+ fps = 30.0
112
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
113
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
114
+
115
+ out_w = ensure_even_dimension(max(1, int(width * downscale_factor)))
116
+ out_h = ensure_even_dimension(max(1, int(height * downscale_factor)))
117
+
118
+ # Choose FOURCC based on extension
119
+ save_ext_lower = save_ext.lower()
120
+ if save_ext_lower in {"mp4", "m4v"}:
121
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
122
+ elif save_ext_lower in {"avi"}:
123
+ fourcc = cv2.VideoWriter_fourcc(*"XVID")
124
+ elif save_ext_lower in {"mov"}:
125
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
126
+ else:
127
+ # Fallback
128
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
129
+
130
+ dst_path.parent.mkdir(parents=True, exist_ok=True)
131
+ writer = cv2.VideoWriter(str(dst_path), fourcc, fps, (out_w, out_h))
132
+ if not writer.isOpened():
133
+ cap.release()
134
+ return False
135
+
136
+ ok = True
137
+ try:
138
+ while True:
139
+ ret, frame = cap.read()
140
+ if not ret:
141
+ break
142
+ resized = cv2.resize(frame, (out_w, out_h), interpolation=cv2.INTER_AREA)
143
+ writer.write(resized)
144
+ except Exception:
145
+ ok = False
146
+ finally:
147
+ writer.release()
148
+ cap.release()
149
+
150
+ # If nothing written, treat as failure
151
+ if dst_path.exists() and dst_path.stat().st_size > 0 and ok:
152
+ return True
153
+ try:
154
+ if dst_path.exists():
155
+ dst_path.unlink()
156
+ except Exception:
157
+ pass
158
+ return False
159
+
160
+
161
+ def process_one(
162
+ src: Path,
163
+ dst: Path,
164
+ downscale_factor: float,
165
+ save_ext: str,
166
+ ) -> tuple:
167
+ success = downscale_video(src, dst, downscale_factor, save_ext)
168
+ return success, dst
169
+
170
+
171
+ @contextmanager
172
+ def tqdm_joblib(tqdm_object):
173
+ """Context manager to patch joblib to report into tqdm progress bar."""
174
+ class TqdmBatchCompletionCallback(joblib_parallel.BatchCompletionCallBack):
175
+ def __call__(self, *args, **kwargs):
176
+ tqdm_object.update(n=self.batch_size)
177
+ return super().__call__(*args, **kwargs)
178
+
179
+ old_cb = joblib_parallel.BatchCompletionCallBack
180
+ joblib_parallel.BatchCompletionCallBack = TqdmBatchCompletionCallback
181
+ try:
182
+ yield tqdm_object
183
+ finally:
184
+ joblib_parallel.BatchCompletionCallBack = old_cb
185
+ try:
186
+ tqdm_object.close()
187
+ except Exception:
188
+ pass
189
+
190
+
191
+ def main() -> int:
192
+ args = parse_args()
193
+
194
+ video_dir: Path = args.video_dir
195
+ save_dir: Path = args.save_dir
196
+ ext: str = args.ext
197
+ save_ext: str = args.save_ext
198
+ downscale_factor: float = args.downscale_factor
199
+ si: int = max(0, int(args.si))
200
+ ei = args.ei if args.ei is None else max(0, int(args.ei))
201
+ n_jobs: int = int(args.n_jobs)
202
+
203
+ if not video_dir.exists() or not video_dir.is_dir():
204
+ print(f"ERROR: video_dir does not exist or is not a directory: {video_dir}", file=sys.stderr)
205
+ return 1
206
+
207
+ videos = list_videos(video_dir, ext)
208
+
209
+ # Slice by [si:ei]
210
+ try:
211
+ videos = videos[si:ei]
212
+ except Exception:
213
+ # Fallback if indexing fails
214
+ videos = []
215
+
216
+ if len(videos) == 0:
217
+ print("No videos found.")
218
+ return 0
219
+
220
+ # In debug mode, limit to first 10 from the sliced list
221
+ if args.debug:
222
+ videos = videos[:10]
223
+
224
+ # Build (src, dst) pairs and count already existing
225
+ tasks = []
226
+ skipped_count = 0
227
+ for src in videos:
228
+ try:
229
+ rel = src.relative_to(video_dir)
230
+ except ValueError:
231
+ rel = src.name
232
+ rel_path = Path(rel)
233
+ rel_with_new_ext = change_extension(rel_path, save_ext)
234
+ dst = (save_dir / rel_with_new_ext).resolve()
235
+ if dst.exists() and dst.stat().st_size > 0:
236
+ skipped_count += 1
237
+ continue
238
+ tasks.append((src, dst))
239
+
240
+ total_count = len(videos)
241
+ saved_paths: List[Path] = []
242
+ errors: int = 0
243
+
244
+ with tqdm(total=total_count, desc="Downscaling videos", unit="vid") as pbar:
245
+ # account for already existing outputs
246
+ if skipped_count:
247
+ pbar.update(skipped_count)
248
+
249
+ if len(tasks) > 0:
250
+ with tqdm_joblib(pbar):
251
+ results = Parallel(n_jobs=n_jobs, backend="loky")( \
252
+ delayed(process_one)(src, dst, downscale_factor, save_ext) for (src, dst) in tasks
253
+ )
254
+ for success, dst in results:
255
+ if success:
256
+ saved_paths.append(dst)
257
+ else:
258
+ errors += 1
259
+
260
+ if args.debug:
261
+ print("Saved (debug mode):")
262
+ for p in saved_paths:
263
+ print(str(p))
264
+
265
+ if errors > 0:
266
+ print(f"Completed with {errors} failures out of {len(videos)}", file=sys.stderr)
267
+ return 2
268
+ return 0
269
+
270
+
271
+ if __name__ == "__main__":
272
+ raise SystemExit(main())
273
+
274
+
shared/scripts/downsize_videos.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Downsize videos preserving aspect resolution."""
2
+ import torch
3
+ import torchvision
4
+ from decord import VideoReader
5
+ from glob import glob
6
+ import os
7
+ from os.path import join, basename, exists
8
+ import subprocess
9
+ import numpy as np
10
+ import pandas as pd
11
+ import subprocess
12
+ import ffmpeg
13
+ import time
14
+ import librosa
15
+ from moviepy.editor import VideoFileClip
16
+
17
+ import shared.utils.log as log
18
+ import shared.utils.io as io
19
+
20
+
21
+ # def downsize(input_path, output_path, width=480, height=None, maintain_aspect_ratio=True):
22
+ # """Downsizes a given video."""
23
+
24
+ # # Check if the video exists
25
+ # assert exists(input_path), f"Video {input_path} does not exist."
26
+
27
+ # # Define ffmpeg command to downsize video with width=480 maintaining aspect ratio
28
+ # # And save it at output_path
29
+ # if maintain_aspect_ratio:
30
+ # assert height is None, "Cannot specify height when maintaining aspect ratio."
31
+ # height = -1
32
+
33
+ # (
34
+ # ffmpeg
35
+ # .input(input_path)
36
+ # .output(output_path, preset="ultrafast", vf=f"scale={width}:{height}", loglevel="quiet")
37
+ # .run()
38
+ # )
39
+
40
+ def resize_video_maintain_aspect_ratio(input_path, output_path):
41
+ (
42
+ ffmpeg
43
+ .input(input_path)
44
+ .filter("scale", w=480, h=-2)
45
+ .output(output_path, crf=18, preset="ultrafast", loglevel="quiet")
46
+ .run()
47
+ )
48
+
49
+
50
+ def resize_video_maintain_aspect_ratio_faster(input_path, output_path, width=480):
51
+ (
52
+ ffmpeg
53
+ .input(input_path)
54
+ .filter('scale', width, -1)
55
+ .output(output_path, vcodec='h264_nvenc', preset='fast', pix_fmt='yuv420p')
56
+ .overwrite_output()
57
+ .run(capture_stdout=True)
58
+ )
59
+
60
+
61
+ def resize_video_maintain_aspect_ratio_vanilla(input_path, output_path, width=480):
62
+ (
63
+ ffmpeg
64
+ .input(input_path)
65
+ .filter('scale', width, -1)
66
+ .output(output_path, c="copy")
67
+ .overwrite_output()
68
+ .run(capture_stdout=True)
69
+ )
70
+
71
+
72
+ def resize_video_moviepy(input_path, output_path, width=480):
73
+ # Load the input video
74
+ video = VideoFileClip(input_path)
75
+
76
+ # Resize the video
77
+ video_resized = video.resize(width=width)
78
+
79
+ # Save the resized video
80
+ video_resized.write_videofile(output_path)
81
+
82
+
83
+
84
+ def resize_video_simple(input_path, output_path, width=480):
85
+ command = f"""ffmpeg -loglevel quiet -i {input_path} -vf "scale={width}:-1" -c:a copy {output_path} -y"""
86
+ subprocess.call(command, shell=True)
87
+
88
+
89
+ if __name__ == "__main__":
90
+ import argparse
91
+ parser = argparse.ArgumentParser()
92
+ parser.add_argument(
93
+ "--csv", type=str, required=True,
94
+ help="Path to csv file containing in/out video paths."
95
+ )
96
+ parser.add_argument(
97
+ "--in_colname", type=str, default="input",
98
+ help="column name of input videos.",
99
+ )
100
+ parser.add_argument(
101
+ "--out_colname", type=str, default="output",
102
+ help="column name of output videos.",
103
+ )
104
+ parser.add_argument(
105
+ "--remove_old", action="store_true",
106
+ )
107
+ parser.add_argument(
108
+ "--width", type=int, default=480,
109
+ )
110
+ parser.add_argument(
111
+ "--debug", action="store_true",
112
+ )
113
+ parser.add_argument(
114
+ "--si", type=int, default=None,
115
+ help="Start index.",
116
+ )
117
+ parser.add_argument(
118
+ "--ei", type=int, default=None,
119
+ help="End index.",
120
+ )
121
+ parser.add_argument('--overwrite', action='store_true')
122
+ args = parser.parse_args()
123
+
124
+ print("Width:", args.width)
125
+ assert exists(args.csv), f"File {args.csv} does not exist."
126
+ df = pd.read_csv(args.csv)
127
+ print("> Number of videos:", len(df))
128
+
129
+ si = args.si if args.si is not None else 0
130
+ ei = args.ei if args.ei is not None else len(df)
131
+ print("> Start index:", si)
132
+ print("> End index:", ei)
133
+ df = df.iloc[si:ei]
134
+ print("> Number of videos to downsize:", len(df))
135
+
136
+ ifiles = df[args.in_colname].tolist()
137
+ ofiles = df[args.out_colname].tolist()
138
+ assert len(ifiles) == len(ofiles), \
139
+ "Number of input and output videos must be the same."
140
+
141
+ iterator = log.tqdm_iterator(
142
+ range(len(ifiles)), total=len(ifiles), desc="Downsizing videos",
143
+ )
144
+ for i in iterator:
145
+ ifile, ofile = ifiles[i], ofiles[i]
146
+
147
+ # If ofile == ifile (i.e., edit the same file),
148
+ # then we need to operate on a temporary file
149
+ # which will then be moved to the original file
150
+ replace = ofile == ifile
151
+ if replace:
152
+ ofile_actual = ofile
153
+ ofile = ofile.replace(".mp4", "_temp.mp4")
154
+
155
+ # Check if the video exists
156
+ assert exists(ifile), f"Video {ifile} does not exist."
157
+
158
+ # If output file already exists, skip
159
+ if exists(ofile) and not args.overwrite:
160
+ continue
161
+
162
+ # Make sure output directory exists
163
+ os.makedirs(os.path.dirname(ofile), exist_ok=True)
164
+
165
+ # resize
166
+ start_time = time.time()
167
+ resize_video_simple(ifile, ofile, width=args.width)
168
+ end_time = time.time()
169
+ time_taken = end_time - start_time
170
+ desc = "Time taken {:.2f}s for {}".format(time_taken, basename(ifile))
171
+ iterator.set_description(desc)
172
+
173
+ # If replace, move the temporary file to the original file
174
+ if replace:
175
+ if exists(ofile):
176
+ os.rename(ofile, ofile_actual)
177
+ ofile = ofile_actual
178
+
179
+ if args.debug:
180
+ yold, srold = librosa.load(ifile, offset=1.0, duration=1.0)
181
+ ynew, srnew = librosa.load(ofile, offset=1.0, duration=1.0)
182
+ assert srold == srnew, "Sampling rate mismatch."
183
+ assert len(yold) == len(ynew), "Length mismatch."
184
+ assert np.allclose(yold, ynew), "Audio mismatch."
185
+
186
+ # Try loading the new video file
187
+ vr = VideoReader(ofile)
188
+ frames = vr.get_batch(range(0, 10)).asnumpy()
189
+ assert frames.shape[0] == 10, "Length mismatch."
190
+ assert frames.shape[2] == 480, "Width mismatch."
191
+
192
+ # If remove_old, remove old video
193
+ if args.remove_old:
194
+ os.remove(ifile)
shared/scripts/downsize_videos_simple.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from glob import glob
3
+ import time
4
+ import argparse
5
+ import numpy as np
6
+ import librosa
7
+ from decord import VideoReader
8
+ import shared.utils.log as log
9
+ from os.path import exists, basename
10
+ from natsort import natsorted
11
+
12
+ def resize_video_simple(input_path, output_path, width=480):
13
+ import subprocess
14
+ command = f"""ffmpeg -loglevel quiet -i {input_path} -vf \"scale={width}:-1\" -c:a copy {output_path}"""
15
+ subprocess.call(command, shell=True)
16
+
17
+ def load_pending_videos(tracker_file):
18
+ """Load list of pending videos from tracker file."""
19
+ if not exists(tracker_file):
20
+ return []
21
+
22
+ with open(tracker_file, 'r') as f:
23
+ return [line.strip() for line in f.readlines() if line.strip()]
24
+
25
+ def save_pending_videos(tracker_file, video_paths):
26
+ """Save list of pending videos to tracker file."""
27
+ with open(tracker_file, 'w') as f:
28
+ for path in video_paths:
29
+ f.write(f"{path}\n")
30
+
31
+ def remove_completed_video(tracker_file, completed_video):
32
+ """Remove a completed video from the tracker file."""
33
+ pending_videos = load_pending_videos(tracker_file)
34
+ if completed_video in pending_videos:
35
+ pending_videos.remove(completed_video)
36
+ save_pending_videos(tracker_file, pending_videos)
37
+
38
+ if __name__ == "__main__":
39
+ parser = argparse.ArgumentParser()
40
+ parser.add_argument(
41
+ "--video_dir", type=str, required=True,
42
+ help="Directory containing videos to downsize."
43
+ )
44
+ parser.add_argument(
45
+ "--ext", type=str, default="mp4",
46
+ help="File extension to search for (default: mp4)."
47
+ )
48
+ parser.add_argument(
49
+ "--remove_old", action="store_true",
50
+ help="Remove original video after downsizing."
51
+ )
52
+ parser.add_argument(
53
+ "--width", type=int, default=480,
54
+ help="Width to resize videos to (default: 480)."
55
+ )
56
+ parser.add_argument(
57
+ "--debug", action="store_true",
58
+ help="Run debug checks after downsizing."
59
+ )
60
+ parser.add_argument("--si", type=int, default=None)
61
+ parser.add_argument("--ei", type=int, default=None)
62
+ parser.add_argument(
63
+ "--tracker_file", type=str, default="video_resize_tracker.txt",
64
+ help="Tracker file to keep track of pending videos (default: video_resize_tracker.txt)."
65
+ )
66
+ parser.add_argument(
67
+ "--reset_tracker", action="store_true",
68
+ help="Reset the tracker file and start fresh."
69
+ )
70
+ args = parser.parse_args()
71
+
72
+ assert os.path.isdir(args.video_dir), f"Directory {args.video_dir} does not exist."
73
+
74
+ # Handle tracker file
75
+ if args.reset_tracker and exists(args.tracker_file):
76
+ os.remove(args.tracker_file)
77
+ print(f"> Reset tracker file: {args.tracker_file}")
78
+
79
+ # Check if we have pending videos from previous run
80
+ pending_videos = load_pending_videos(args.tracker_file)
81
+
82
+ if pending_videos:
83
+ print(f"> Found {len(pending_videos)} pending videos from previous run")
84
+ ifiles = pending_videos
85
+ ofiles = pending_videos # In-place replacement
86
+ else:
87
+ # Start fresh - find all videos
88
+ pattern = os.path.join(args.video_dir, f"**/*.{args.ext}")
89
+ ifiles = glob(pattern, recursive=True)
90
+ ifiles = natsorted(ifiles)
91
+ ofiles = ifiles # In-place replacement
92
+ print("> Number of videos in the directory:", len(ifiles))
93
+
94
+ # Apply start/end index filtering
95
+ si = args.si if args.si is not None else 0
96
+ ei = args.ei if args.ei is not None else len(ifiles)
97
+ print("> Start index:", si)
98
+ print("> End index:", ei)
99
+ ifiles = ifiles[si:ei]
100
+ ofiles = ofiles[si:ei]
101
+
102
+ # Save to tracker file for future runs
103
+ save_pending_videos(args.tracker_file, ifiles)
104
+ print(f"> Saved {len(ifiles)} videos to tracker file: {args.tracker_file}")
105
+
106
+ print("> Number of videos to downsize:", len(ifiles))
107
+
108
+ iterator = log.tqdm_iterator(
109
+ range(len(ifiles)), total=len(ifiles), desc="Downsizing videos",
110
+ )
111
+ for i in iterator:
112
+ ifile = ifiles[i]
113
+ ofile = ifile
114
+ assert exists(ifile), f"Video {ifile} does not exist."
115
+ tmp_ofile = ifile + ".tmp.mp4"
116
+ start_time = time.time()
117
+ resize_video_simple(ifile, tmp_ofile, width=args.width)
118
+ end_time = time.time()
119
+ time_taken = end_time - start_time
120
+ desc = f"Time taken {time_taken:.2f}s for {basename(ifile)}"
121
+ iterator.set_description(desc)
122
+
123
+ if args.debug:
124
+ yold, srold = librosa.load(ifile, offset=1.0, duration=1.0)
125
+ ynew, srnew = librosa.load(tmp_ofile, offset=1.0, duration=1.0)
126
+ assert srold == srnew, "Sampling rate mismatch."
127
+ assert len(yold) == len(ynew), "Length mismatch."
128
+ assert np.allclose(yold, ynew), "Audio mismatch."
129
+ vr = VideoReader(tmp_ofile)
130
+ frames = vr.get_batch(range(0, 10)).asnumpy()
131
+ assert frames.shape[0] == 10, "Length mismatch."
132
+ assert frames.shape[2] == args.width, "Width mismatch."
133
+
134
+ # Replace original file
135
+ os.replace(tmp_ofile, ofile)
136
+
137
+ # Remove completed video from tracker
138
+ remove_completed_video(args.tracker_file, ifile)
139
+
140
+ if args.remove_old:
141
+ # Already replaced, so nothing to remove
142
+ pass
143
+
144
+ # Clean up tracker file if all videos are done
145
+ if not load_pending_videos(args.tracker_file):
146
+ os.remove(args.tracker_file)
147
+ print(f"> All videos completed. Removed tracker file: {args.tracker_file}")
148
+ else:
149
+ remaining = len(load_pending_videos(args.tracker_file))
150
+ print(f"> {remaining} videos remaining. Tracker file preserved: {args.tracker_file}")
shared/scripts/extract_speed_clips.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from glob import glob
4
+ from collections import defaultdict
5
+
6
+ import json
7
+ from hydra import compose, initialize
8
+ from omegaconf import OmegaConf
9
+ import PIL, PIL.Image
10
+ import decord
11
+
12
+ from adapt4change.utils.speednet import *
13
+ import shared.utils as su
14
+
15
+ classes_selected = [
16
+ "skipping rope",
17
+ "gymnastics tumbling",
18
+ "somersaulting",
19
+ "cartwheeling",
20
+ "trampolines bouncing",
21
+ "swinging on something",
22
+ "vault",
23
+ "deadlifting",
24
+ "clean and jerk",
25
+ "diving cliff",
26
+ ]
27
+
28
+ SAVE_DIR = "/scratch/shared/beegfs/piyush/datasets/SpeedyKinetics/clips"
29
+ os.makedirs(SAVE_DIR, exist_ok=True)
30
+
31
+ def check_all_files_exist(save_paths):
32
+ for path in save_paths:
33
+ if not os.path.exists(path):
34
+ return False
35
+ return True
36
+
37
+ def save_clips_for_single_video(video_path, show=False):
38
+ """
39
+ Note that I randomly sample 3s clips out of the original video.
40
+ """
41
+ video_id = os.path.basename(video_path).split(".mp4")[0]
42
+ save_paths = [
43
+ f"{SAVE_DIR}/{video_id}-normal.mp4",
44
+ f"{SAVE_DIR}/{video_id}-spedup.mp4",
45
+ f"{SAVE_DIR}/{video_id}-slowdn.mp4",
46
+ ]
47
+ if check_all_files_exist(save_paths):
48
+ return
49
+
50
+ try:
51
+ vr = decord.VideoReader(video_path)
52
+ except Exception as e:
53
+ print(f"Error opening video {video_path}: {e}")
54
+ return
55
+
56
+ total_frames = len(vr)
57
+ fps = vr.get_avg_fps()
58
+
59
+ # Initialize sampler for a video
60
+ sampler = FrameIndexSampler(total_frames=total_frames)
61
+
62
+ # Sample clips
63
+ clip_duration = 3.
64
+ T = int(clip_duration * fps)
65
+ start_frame = sampler.get_valid_start_frame(T)
66
+
67
+ # Get all clip indices
68
+ normal_indices, sped_up_indices, slowed_down_indices = sampler.sample_all_clip_indices(start_frame, T)
69
+
70
+ try:
71
+ frames_normal = [PIL.Image.fromarray(f) for f in vr.get_batch(normal_indices).asnumpy()]
72
+ frames_spedup = [PIL.Image.fromarray(f) for f in vr.get_batch(sped_up_indices).asnumpy()]
73
+ frames_slowdn = [PIL.Image.fromarray(f) for f in vr.get_batch(slowed_down_indices).asnumpy()]
74
+ except Exception as e:
75
+ print(f"Error processing video {video_path}: {e}")
76
+ return
77
+
78
+ su.io.save_video(frames_normal, save_paths[0], fps=vr.get_avg_fps())
79
+ su.io.save_video(frames_spedup, save_paths[1], fps=vr.get_avg_fps())
80
+ su.io.save_video(frames_slowdn, save_paths[2], fps=vr.get_avg_fps())
81
+ if show:
82
+ su.visualize.show_grid_of_videos(
83
+ files=save_paths,
84
+ labels=["Normal", "Sped up", "Slowed down"],
85
+ )
86
+
87
+
88
+ if __name__ == "__main__":
89
+ import argparse
90
+ parser = argparse.ArgumentParser()
91
+ parser.add_argument("--start_index", type=int, default=0)
92
+ parser.add_argument("--end_index", type=int, default=1000000)
93
+ args = parser.parse_args()
94
+
95
+ data_dir = "/datasets/KineticsClean/"
96
+ verbose = True
97
+ total_train = []
98
+ total_valid = []
99
+ for c in classes_selected:
100
+ files_train = glob(f"{data_dir}/train_split/{c}/*.mp4")
101
+ files_valid = glob(f"{data_dir}/val_split/{c}/*.mp4")
102
+ if verbose:
103
+ print(c)
104
+ print("Train videos: ", len(files_train))
105
+ print("Valid videos: ", len(files_valid))
106
+ print("-" * 80)
107
+ total_train.extend(files_train)
108
+ total_valid.extend(files_valid)
109
+ print("Total train files: ", len(total_train))
110
+ print("Total valid files: ", len(total_valid))
111
+
112
+ files = total_train + total_valid
113
+ print(f"Total files: {len(files)}")
114
+ print(f"Start index: {args.start_index}")
115
+ print(f"End index: {args.end_index}")
116
+ files = files[args.start_index:args.end_index]
117
+ print(f"Total files to process: {len(files)}")
118
+
119
+ parallelize = True
120
+ if not parallelize:
121
+ for file in su.log.tqdm_iterator(files, desc="Processing files"):
122
+ save_clips_for_single_video(file)
123
+ else:
124
+ from joblib import Parallel, delayed
125
+ iterator = su.log.tqdm_iterator(files, desc="Processing files")
126
+ Parallel(n_jobs=16)(delayed(save_clips_for_single_video)(file) for file in iterator)
shared/scripts/save_grid_of_videos.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Video Grid Visualizer (imageio-based with Grid)
4
+
5
+ Creates a grid layout of multiple videos with visual separators and saves it
6
+ as an MP4 or GIF. This version uses the imageio library for robust video
7
+ writing, adds customizable grid gaps, and intelligently resizes cells for a
8
+ balanced look.
9
+ """
10
+
11
+ import cv2
12
+ import numpy as np
13
+ import argparse
14
+ import os
15
+ import imageio.v2 as imageio # Use imageio.v2 to avoid deprecation warnings
16
+
17
+ def get_video_info(video_path):
18
+ """Get video information using imageio with a fallback to OpenCV."""
19
+ try:
20
+ with imageio.get_reader(video_path) as reader:
21
+ meta = reader.get_meta_data()
22
+ fps = meta.get('fps', 30)
23
+ duration = meta.get('duration', 0)
24
+ size = meta.get('size', (0, 0))
25
+ if duration == 0 and fps > 0:
26
+ duration = reader.count_frames() / fps
27
+ frame_count = int(duration * fps) if duration and fps else reader.count_frames()
28
+
29
+ return {
30
+ 'duration': duration, 'fps': fps, 'frame_count': frame_count,
31
+ 'width': size[0], 'height': size[1]
32
+ }
33
+ except Exception:
34
+ cap = cv2.VideoCapture(video_path)
35
+ if not cap.isOpened():
36
+ raise ValueError(f"Cannot open video: {video_path}")
37
+
38
+ fps = cap.get(cv2.CAP_PROP_FPS)
39
+ frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
40
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
41
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
42
+ duration = frame_count / fps if fps > 0 else 0
43
+ cap.release()
44
+ return {
45
+ 'duration': duration, 'fps': fps, 'frame_count': frame_count,
46
+ 'width': width, 'height': height
47
+ }
48
+
49
+ def resize_frame(frame, target_width, target_height):
50
+ """Resize frame to fit inside target dimensions, maintaining aspect ratio."""
51
+ h, w = frame.shape[:2]
52
+ aspect = w / h
53
+ target_aspect = target_width / target_height
54
+
55
+ if aspect > target_aspect:
56
+ new_w = target_width
57
+ new_h = int(new_w / aspect)
58
+ else:
59
+ new_h = target_height
60
+ new_w = int(new_h * aspect)
61
+
62
+ resized = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_AREA)
63
+
64
+ canvas = np.zeros((target_height, target_width, 3), dtype=np.uint8)
65
+ y_offset = (target_height - new_h) // 2
66
+ x_offset = (target_width - new_w) // 2
67
+ canvas[y_offset:y_offset + new_h, x_offset:x_offset + new_w] = resized
68
+
69
+ return canvas
70
+
71
+ def create_video_grid(video_paths, n_rows=1, n_cols=None, save_path="output.mp4",
72
+ gap=10, grid_color_name="white", max_cell_width=640):
73
+ """
74
+ Create a grid of videos with separators and save as MP4 or GIF.
75
+ """
76
+ if not video_paths:
77
+ raise ValueError("No video paths provided")
78
+
79
+ if n_cols is None:
80
+ n_cols = len(video_paths)
81
+
82
+ if n_rows * n_cols < len(video_paths):
83
+ raise ValueError(f"Grid size ({n_rows}x{n_cols}) is too small for {len(video_paths)} videos")
84
+
85
+ grid_color = (255, 255, 255) if grid_color_name.lower() == "white" else (0, 0, 0)
86
+
87
+ print("Gathering video information...")
88
+ video_infos = [get_video_info(path) for path in video_paths]
89
+
90
+ # --- Intelligent Resizing and Dimension Calculation ---
91
+ valid_infos = [info for info in video_infos if info['width'] > 0 and info['height'] > 0]
92
+ if not valid_infos:
93
+ raise ValueError("Could not get valid dimensions from any input video.")
94
+
95
+ avg_aspect_ratio = sum(info['width'] / info['height'] for info in valid_infos) / len(valid_infos)
96
+
97
+ cell_width = min(max_cell_width, 1920 // n_cols) # Don't let cells get too big
98
+ cell_height = int(cell_width / avg_aspect_ratio)
99
+
100
+ output_width = (cell_width * n_cols) + (gap * (n_cols + 1))
101
+ output_height = (cell_height * n_rows) + (gap * (n_rows + 1))
102
+
103
+ # Ensure dimensions are even, as required by many video codecs
104
+ output_width += output_width % 2
105
+ output_height += output_height % 2
106
+ # ---
107
+
108
+ max_duration = max(info['duration'] for info in video_infos if info)
109
+ target_fps = max(info['fps'] for info in video_infos if info and info['fps'])
110
+ if not target_fps or target_fps <= 0:
111
+ target_fps = 30
112
+
113
+ total_frames = int(max_duration * target_fps)
114
+
115
+ print(f"Grid: {n_rows}x{n_cols} with {gap}px {grid_color_name} gaps")
116
+ print(f"Calculated Cell Size: {cell_width}x{cell_height}")
117
+ print(f"Final Output Size: {output_width}x{output_height}")
118
+ print(f"Max duration: {max_duration:.2f}s | Target FPS: {target_fps} | Total frames: {total_frames}")
119
+
120
+ caps = [cv2.VideoCapture(path) for path in video_paths]
121
+ last_frames = [None] * len(video_paths)
122
+
123
+ writer = imageio.get_writer(save_path, fps=target_fps, codec='libx264', macro_block_size=None)
124
+
125
+ try:
126
+ for frame_idx in range(total_frames):
127
+ # Initialize the master frame with the grid color
128
+ output_frame = np.full((output_height, output_width, 3), grid_color, dtype=np.uint8)
129
+
130
+ for i, cap in enumerate(caps):
131
+ ret, frame = cap.read()
132
+ if ret:
133
+ last_frames[i] = frame
134
+ else:
135
+ if last_frames[i] is None:
136
+ info = video_infos[i]
137
+ h = info['height'] if info['height'] > 0 else cell_height
138
+ w = info['width'] if info['width'] > 0 else cell_width
139
+ last_frames[i] = np.zeros((h, w, 3), dtype=np.uint8)
140
+ frame = last_frames[i]
141
+
142
+ resized_frame = resize_frame(frame, cell_width, cell_height)
143
+
144
+ row = i // n_cols
145
+ col = i % n_cols
146
+
147
+ # Calculate position with gaps
148
+ y_start = gap + row * (cell_height + gap)
149
+ x_start = gap + col * (cell_width + gap)
150
+
151
+ output_frame[y_start : y_start + cell_height, x_start : x_start + cell_width] = resized_frame
152
+
153
+ rgb_frame = cv2.cvtColor(output_frame, cv2.COLOR_BGR2RGB)
154
+ writer.append_data(rgb_frame)
155
+
156
+ if frame_idx % int(target_fps) == 0 or frame_idx == total_frames - 1:
157
+ progress = (frame_idx + 1) / total_frames * 100
158
+ print(f"Processing... {progress:.1f}% complete", end='\r')
159
+
160
+ finally:
161
+ print("\nCleaning up resources...")
162
+ for cap in caps:
163
+ cap.release()
164
+ writer.close()
165
+
166
+ print(f"Video grid successfully saved to: {save_path}")
167
+
168
+ def main():
169
+ parser = argparse.ArgumentParser(
170
+ description="Create a grid visualization of multiple videos with separators.",
171
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter
172
+ )
173
+ parser.add_argument("video_paths", nargs="+", help="One or more paths to input videos.")
174
+ parser.add_argument("--n_rows", type=int, default=1, help="Number of rows in the grid.")
175
+ parser.add_argument("--n_cols", type=int, help="Number of columns. Defaults to the number of videos if n_rows is 1.")
176
+ parser.add_argument("--save_path", default="output.mp4", help="Output path for the video (e.g., 'output.mp4' or 'output.gif').")
177
+ parser.add_argument("--gap", type=int, default=10, help="Size of the gap between videos in pixels.")
178
+ parser.add_argument("--grid_color", default="white", choices=["white", "black"], help="Color of the grid gaps.")
179
+ parser.add_argument("--max_cell_width", type=int, default=640, help="Maximum width for each video cell in the grid.")
180
+
181
+ args = parser.parse_args()
182
+
183
+ # Set default n_cols if not provided
184
+ if args.n_cols is None:
185
+ args.n_cols = len(args.video_paths) // args.n_rows
186
+ if len(args.video_paths) % args.n_rows != 0:
187
+ args.n_cols += 1
188
+
189
+ try:
190
+ create_video_grid(
191
+ video_paths=args.video_paths,
192
+ n_rows=args.n_rows,
193
+ n_cols=args.n_cols,
194
+ save_path=args.save_path,
195
+ gap=args.gap,
196
+ grid_color_name=args.grid_color,
197
+ max_cell_width=args.max_cell_width
198
+ )
199
+ except Exception as e:
200
+ print(f"\nAn error occurred: {e}")
201
+
202
+ if __name__ == "__main__":
203
+ main()
shared/scripts/shard_video_dataset.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Creates multiple shards of videos in a dataset.
3
+
4
+ Help:
5
+
6
+ input_dir=/work/piyush/from_nfs2/datasets/SSv2/20bn-something-something-v2/
7
+ output_dir=/work/piyush/from_nfs2/datasets/SSv2/ssv2_shards/
8
+ python shared/scripts/shard_video_dataset.py -i $input_dir -o $output_dir
9
+ """
10
+ import os
11
+ import tarfile
12
+ from glob import glob
13
+
14
+ import shared.utils as su
15
+
16
+
17
+ def read_args():
18
+ import argparse
19
+ parser = argparse.ArgumentParser()
20
+ # Input video directory
21
+ parser.add_argument("-i", "--input_dir", type=str, required=True)
22
+ parser.add_argument("--ext", type=str, default="webm")
23
+ # Output shard directory
24
+ parser.add_argument("-o", "--output_dir", type=str, required=True)
25
+ # Max size of each shard (n.o. videos)
26
+ parser.add_argument("--shard_size", type=int, default=10000)
27
+ args = parser.parse_args()
28
+ return args
29
+
30
+
31
+ # Iterate over videos in your dataset
32
+ def write_to_shard(tar, video_path, video_name):
33
+ with open(video_path, 'rb') as f:
34
+ tarinfo = tarfile.TarInfo(name=video_name)
35
+ tarinfo.size = os.path.getsize(video_path)
36
+ tar.addfile(tarinfo, f)
37
+
38
+
39
+ if __name__ == "__main__":
40
+
41
+ # Read arguments
42
+ args = read_args()
43
+
44
+ # Create output directory
45
+ os.makedirs(args.output_dir, exist_ok=True)
46
+
47
+ # Get all video files
48
+ su.log.print_update("Processing files at " + args.input_dir)
49
+ video_files = glob(os.path.join(args.input_dir, f"*.{args.ext}"))
50
+ print(f"Found {len(video_files)} video files")
51
+
52
+ # Iterate
53
+ iterator = su.log.tqdm_iterator(video_files, desc="Sharding videos")
54
+
55
+ shard_size = args.shard_size
56
+ output_dir = args.output_dir
57
+ shard_id = 0
58
+ i = 0
59
+ for video_file in iterator:
60
+ if i % shard_size == 0:
61
+ if i > 0:
62
+ tar.close()
63
+ shard_path = os.path.join(output_dir, f"shard-{shard_id:04d}.tar")
64
+ print(f"Creating shard {shard_path}")
65
+ tar = tarfile.open(shard_path, 'w')
66
+ shard_id += 1
67
+ write_to_shard(tar, video_file, os.path.basename(video_file))
68
+ i += 1
69
+
70
+ if tar:
71
+ tar.close()
shared/utils/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shared.utils.paths as paths
2
+ import shared.utils.log as log
3
+ import shared.utils.io as io
4
+ # import shared.utils.audio as audio
5
+ import shared.utils.image as image
6
+ # import shared.utils.av as av
7
+ import shared.utils.pandas_utils as pd_utils
8
+ import shared.utils.visualize as visualize
9
+ import shared.utils.metrics as metrics
10
+ import shared.utils.misc as misc
11
+ # import shared.utils.keypoint_matching as keypoint_matching
12
+ import shared.utils.physics as physics
13
+ import shared.utils.video as video
14
+ import shared.utils.visual_prompts as visual_prompts
15
+ import shared.utils.gif as gif
16
+
shared/utils/audio.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Audio utils"""
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+
5
+
6
+ def load_audio(audio_path: str, sr: int = None, max_duration: int = 10., start: int = 0, stop: int = None):
7
+ """Loads audio and pads/trims it to max_duration"""
8
+ import librosa
9
+ data, sr = librosa.load(audio_path, sr=sr)
10
+
11
+ if stop is not None:
12
+ start = int(start * sr)
13
+ stop = int(stop * sr)
14
+ data = data[start:stop]
15
+
16
+ # Convert to mono
17
+ if len(data.shape) > 1:
18
+ data = np.mean(data, axis=1)
19
+
20
+ n_frames = int(max_duration * sr)
21
+ if len(data) > n_frames:
22
+ data = data[:n_frames]
23
+ elif len(data) < n_frames:
24
+ data = np.pad(data, (0, n_frames - len(data)), "constant")
25
+ return data, sr
26
+
27
+
28
+ # def compute_spectrogram(data: np.ndarray, sr: int):
29
+ # D = librosa.stft(data) # STFT of y
30
+ # S_db = librosa.amplitude_to_db(np.abs(D), ref=np.max)
31
+ # return S_db
32
+
33
+
34
+ def compute_spec_freq_mean(S_db: np.ndarray, eps=1e-5):
35
+ # Compute mean of spectrogram over frequency axis
36
+ S_db_normalized = (S_db - S_db.mean(axis=1)[:, None]) / (S_db.std(axis=1)[:, None] + eps)
37
+ S_db_over_time = S_db_normalized.sum(axis=0)
38
+ return S_db_over_time
39
+
40
+
41
+ def process_audiofile(audio_path, functions=["load_audio", "compute_spectrogram", "compute_spec_freq_mean"]):
42
+ """Processes audio file with a list of functions"""
43
+ data, sr = load_audio(audio_path)
44
+ for function in functions:
45
+ if function == "load_audio":
46
+ pass
47
+ elif function == "compute_spectrogram":
48
+ data = compute_spectrogram(data, sr)
49
+ elif function == "compute_spec_freq_mean":
50
+ data = compute_spec_freq_mean(data)
51
+ else:
52
+ raise ValueError(f"Unknown function {function}")
53
+ return data
54
+
55
+
56
+
57
+ """PyDub's silence detection is based on the energy of the audio signal."""
58
+ import numpy as np
59
+
60
+
61
+ def sigmoid(x):
62
+ return 1 / (1 + np.exp(-x))
63
+
64
+
65
+ class SilenceDetector:
66
+
67
+
68
+ def __init__(self, silence_thresh=-36) -> None:
69
+ self.silence_thresh = silence_thresh
70
+
71
+ def __call__(self, audio_path: str, start=None, end=None):
72
+
73
+ import pydub
74
+ from pydub.utils import db_to_float
75
+
76
+ try:
77
+ waveform = pydub.AudioSegment.from_file(audio_path)
78
+ except:
79
+ print("Error loading audio file: ", audio_path)
80
+ return 100.0
81
+
82
+ start_ms = int(start * 1000) if start else 0
83
+ end_ms = int(end * 1000) if end else len(waveform)
84
+ waveform = waveform[start_ms:end_ms]
85
+
86
+ # convert silence threshold to a float value (so we can compare it to rms)
87
+ silence_thresh = db_to_float(self.silence_thresh) * waveform.max_possible_amplitude
88
+
89
+ if waveform.rms == 0:
90
+ return 100.0
91
+
92
+ silence_prob = sigmoid((silence_thresh - waveform.rms) / waveform.rms)
93
+
94
+ # return waveform.rms <= silence_thresh
95
+ return np.round(100 * silence_prob, 2)
96
+
97
+
98
+ def frequency_bin_to_value(bin_index, sr, n_fft):
99
+ return int(bin_index * sr / n_fft)
100
+
101
+
102
+ def time_bin_to_value(bin_index, hop_length, sr):
103
+ return (bin_index) * (hop_length / sr)
104
+
105
+
106
+ def add_time_annotations(ax, nt_bins, hop_length, sr, skip=50):
107
+ # Show time (s) values on the x-axis
108
+ t_bins = np.arange(nt_bins)
109
+ t_vals = np.round(np.array([time_bin_to_value(tb, hop_length, sr) for tb in t_bins]), 1)
110
+ try:
111
+ ax.set_xticks(t_bins[::skip], t_vals[::skip])
112
+ except:
113
+ pass
114
+ ax.set_xlabel("Time (s)")
115
+
116
+
117
+ def add_freq_annotations(ax, nf_bins, sr, n_fft, skip=50):
118
+ f_bins = np.arange(nf_bins)
119
+ f_vals = np.array([frequency_bin_to_value(fb, sr, n_fft) for fb in f_bins])
120
+ try:
121
+ ax.set_yticks(f_bins[::skip], f_vals[::skip])
122
+ except:
123
+ pass
124
+ # ax.set_yticks(f_bins[::skip])
125
+ # ax.set_yticklabels(f_vals[::skip])
126
+ ax.set_ylabel("Frequency (Hz)")
127
+
128
+
129
+ def show_single_spectrogram(
130
+ spec,
131
+ sr,
132
+ n_fft,
133
+ hop_length,
134
+ ax=None,
135
+ fig=None,
136
+ figsize=(10, 2),
137
+ cmap="viridis",
138
+ colorbar=True,
139
+ show=True,
140
+ format='%+2.0f dB',
141
+ xlabel='Time (s)',
142
+ ylabel="Frequency (Hz)",
143
+ title=None,
144
+ show_dom_freq=False,
145
+ ):
146
+
147
+ if ax is None:
148
+ fig, ax = plt.subplots(1, 1, figsize=figsize)
149
+ axim = ax.imshow(spec, origin="lower", cmap=cmap)
150
+
151
+ # Show frequency (Hz) values on y-axis
152
+ nf_bins, nt_bins = spec.shape
153
+
154
+ if "frequency" in ylabel.lower():
155
+ # Add frequency annotation
156
+ add_freq_annotations(ax, nf_bins, sr, n_fft)
157
+
158
+ # Add time annotation
159
+ add_time_annotations(ax, nt_bins, hop_length, sr)
160
+
161
+ ax.set_title(title)
162
+ ax.set_xlabel(xlabel)
163
+ ax.set_ylabel(ylabel)
164
+
165
+ if colorbar:
166
+ fig.colorbar(axim, ax=ax, orientation='vertical', fraction=0.01, format=format)
167
+
168
+ if show_dom_freq:
169
+ fmax = spec.argmax(axis=0)
170
+ ax.scatter(np.arange(spec.shape[1]), fmax, color="white", s=0.2)
171
+
172
+ if show:
173
+ plt.show()
174
+
175
+
176
+ def compute_spectrogram(y, n_fft, hop_length, margin, n_mels=None):
177
+ import librosa
178
+
179
+ # STFT
180
+ D = librosa.stft(y, n_fft=n_fft, hop_length=hop_length)
181
+
182
+ # Run HPSS
183
+ S, _ = librosa.decompose.hpss(D, margin=margin)
184
+
185
+ # DB
186
+ S = librosa.amplitude_to_db(np.abs(S), ref=np.max)
187
+
188
+ if n_mels is not None:
189
+ S = librosa.feature.melspectrogram(S=S, n_mels=n_mels)
190
+
191
+ return S
192
+
193
+
194
+ def show_spectrogram(S, sr, n_fft=512, hop_length=256, figsize=(10, 3), n_mels=None, ax=None, show=True):
195
+ import librosa
196
+ if ax is None:
197
+ fig, ax = plt.subplots(1, 1, figsize=figsize)
198
+ y_axis = "mel" if n_mels is not None else "linear"
199
+ librosa.display.specshow(
200
+ S,
201
+ sr=sr,
202
+ hop_length=hop_length,
203
+ n_fft=n_fft,
204
+ y_axis=y_axis,
205
+ x_axis='time',
206
+ ax=ax,
207
+ )
208
+ ax.set_title("LogSpectrogram" if n_mels is None else "LogMelSpectrogram")
209
+ if show:
210
+ plt.show()
211
+
212
+
213
+ def show_frame_and_spectrogram(frame, S, sr, figsize=(12, 4), show=True, axes=None, **spec_args):
214
+ if axes is None:
215
+ fig, axes = plt.subplots(1, 2, figsize=figsize, gridspec_kw={"width_ratios": [0.2, 0.8]})
216
+ ax = axes[0]
217
+ ax.imshow(frame)
218
+ ax.set_xticks([])
219
+ ax.set_yticks([])
220
+
221
+ ax = axes[1]
222
+ show_spectrogram(S=S, sr=sr, ax=ax, show=False, **spec_args)
223
+
224
+ plt.tight_layout()
225
+
226
+ if show:
227
+ plt.show()
shared/utils/av.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Audio-visual helper functions."""
2
+ import cv2
3
+ import numpy as np
4
+
5
+
6
+ def save_video_with_audio(video, audio, output_path):
7
+ """
8
+ Saves a video file with audio.
9
+
10
+ Args:
11
+ video (np.ndarray): Video frames.
12
+ audio (np.ndarray): Audio samples.
13
+ output_path (str): Output path.
14
+ """
15
+
16
+ # check the correct shape and format for audio
17
+ assert isinstance(audio, np.ndarray)
18
+ assert len(audio.shape) == 2
19
+ assert audio.shape[1] in [1, 2]
20
+
21
+ # create video writer
22
+ video_writer = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), 30, (video.shape[2], video.shape[1]))
23
+ # write the image frames to the video
24
+ for frame in video:
25
+ video_writer.write(frame)
26
+ # add the audio data to the video
27
+ video_writer.write(audio)
28
+ # release the VideoWriter object
29
+ video_writer.release()
30
+
31
+
32
+ def save_video_from_image_sequence_and_audio(sequence, audio, save_path, video_fps=15, audio_fps=22100):
33
+ import torch
34
+ from moviepy.editor import VideoClip, AudioClip, ImageSequenceClip
35
+ from moviepy.audio.AudioClip import AudioArrayClip
36
+
37
+ assert isinstance(sequence, list) and isinstance(audio, (np.ndarray, torch.Tensor))
38
+ assert len(audio.shape) == 2 and audio.shape[1] in [1, 2]
39
+
40
+ video_duration = len(sequence) / video_fps
41
+ audio_duration = len(audio) / audio_fps
42
+ # # print(f"Video duration: {video_duration:.2f}s, audio duration: {audio_duration:.2f}s")
43
+ # assert video_duration == audio_duration, \
44
+ # f"Video duration ({video_duration}) and audio duration ({audio_duration}) do not match."
45
+
46
+ video_clip = ImageSequenceClip(sequence, fps=video_fps)
47
+ audio_clip = AudioArrayClip(audio, fps=audio_fps)
48
+ video_clip = video_clip.set_audio(audio_clip)
49
+ # video_clip.write_videofile(save_path, verbose=True, logger=None, fps=video_fps, audio_fps=audio_fps)
50
+ video_clip.write_videofile(save_path, verbose=False, logger=None)
51
+
52
+
53
+ import cv2, os
54
+ import argparse
55
+ import numpy as np
56
+ from glob import glob
57
+ import librosa
58
+ import subprocess
59
+
60
+
61
+ def generate_video(args):
62
+
63
+ frames = glob('{}/*.png'.format(args.input_dir))
64
+ print("Total frames = ", len(frames))
65
+
66
+ frames.sort(key = lambda x: int(x.split("/")[-1].split(".")[0]))
67
+
68
+ img = cv2.imread(frames[0])
69
+ print(img.shape)
70
+ fname = 'inference.avi'
71
+ video = cv2.VideoWriter(
72
+ fname, cv2.VideoWriter_fourcc(*'DIVX'), args.fps, (img.shape[1], img.shape[0]),
73
+ )
74
+
75
+ for i in range(len(frames)):
76
+ img = cv2.imread(frames[i])
77
+ video.write(img)
78
+
79
+ video.release()
80
+
81
+ output_file_name = args.output_video
82
+
83
+ no_sound_video = output_file_name + '_nosound.mp4'
84
+ subprocess.call('ffmpeg -hide_banner -loglevel panic -i %s -c copy -an -strict -2 %s' % (fname, no_sound_video), shell=True)
85
+
86
+ if args.audio_file is not None:
87
+ video_output = output_file_name + '.mp4'
88
+ subprocess.call('ffmpeg -hide_banner -loglevel panic -y -i %s -i %s -strict -2 -q:v 1 %s' %
89
+ (args.audio_file, no_sound_video, video_output), shell=True)
90
+
91
+ os.remove(no_sound_video)
92
+
93
+ os.remove(fname)
shared/utils/classification.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helper functions for classification tasks."""
2
+ import matplotlib.pyplot as plt
3
+ import pandas as pd
4
+ import numpy as np
5
+
6
+
7
+ def plot_metric_curve(
8
+ xvalues, yvalues, thresholds, title=None,
9
+ figsize=(8, 7), show_thresholds=True, show_legend=True,
10
+ ylabel='X', xlabel='Y', ax=None, text_delta=0.01,
11
+ label="Metric Curve", color="royalblue", show=False,
12
+ fill=None,
13
+ ):
14
+ """Plot a metric curve, e.g., PR curve or ROC curve."""
15
+
16
+ if ax is None:
17
+ fig, ax = plt.subplots(1, 1, figsize=figsize)
18
+
19
+ ax.grid(alpha=0.3)
20
+ ax.set_title(title)
21
+ ax.set_ylabel(ylabel)
22
+ ax.set_xlabel(xlabel)
23
+
24
+ ax.plot(xvalues, yvalues, marker='o', label=label, color=color)
25
+ ax.set_xlim(-0.08, 1.08)
26
+ ax.set_ylim(-0.08, 1.08)
27
+
28
+ if fill is not None:
29
+ yticks = ax.get_yticks()
30
+ ax.fill_between(xvalues, yvalues, "", alpha=0.08, color=color)
31
+ # Add `fill` inside the curve
32
+ # Find a single (x, y) s.t. it is inside the curve
33
+ ax.text(0.4, 0.5, fill, color=color)
34
+ ax.set_yticks(yticks)
35
+ ax.set_yticklabels([f"{y:.1f}" for y in yticks])
36
+ ax.set_ylim(-0.08, 1.08)
37
+
38
+ # Show thresholds
39
+ if show_thresholds:
40
+ for x, y, t in zip(xvalues, yvalues, thresholds):
41
+ ax.text(x + text_delta, y + text_delta, np.round(t, 2), color=color, alpha=0.5)
42
+
43
+ if show_legend:
44
+ ax.legend()
45
+
46
+ if show:
47
+ plt.show()
shared/utils/epic.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utils specific for EPIC data."""
2
+ import datetime
3
+
4
+
5
+ def timestamp_to_seconds(timestamp: str):
6
+ # Parse the timestamp string into a datetime object
7
+ time_obj = datetime.datetime.strptime(timestamp, '%H:%M:%S.%f')
8
+
9
+ # Calculate the total number of seconds using the timedelta object
10
+ total_seconds = time_obj.time().second \
11
+ + time_obj.time().minute * 60 \
12
+ + time_obj.time().hour * 3600 \
13
+ + time_obj.time().microsecond / 1000000
14
+
15
+ return total_seconds
shared/utils/gif.py ADDED
@@ -0,0 +1,609 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import os
3
+ from pathlib import Path
4
+
5
+ def create_side_by_side_gif(video_paths, output_gif, gap_width=20, fps=10, scale_height=240, gap_color="white", verbose=False):
6
+ """
7
+ Create a single GIF with multiple videos placed side by side.
8
+
9
+ Args:
10
+ video_paths (list): List of paths to input MP4 files
11
+ output_gif (str): Path for output GIF file
12
+ gap_width (int): Width of gap between videos in pixels
13
+ fps (int): Frame rate for output GIF
14
+ scale_height (int): Height to scale all videos to (maintains aspect ratio)
15
+ gap_color (str): Color for gaps between videos (e.g., "white", "black", "red", "#FF0000")
16
+ verbose (bool): Whether to print FFmpeg commands and processing messages
17
+ """
18
+
19
+ if not video_paths:
20
+ raise ValueError("No video paths provided")
21
+
22
+ # Verify all input files exist
23
+ for path in video_paths:
24
+ if not os.path.exists(path):
25
+ raise FileNotFoundError(f"Video file not found: {path}")
26
+
27
+ # Create filter complex string for FFmpeg
28
+ num_videos = len(video_paths)
29
+
30
+ # Input mapping and scaling
31
+ filter_parts = []
32
+ scaled_inputs = []
33
+
34
+ for i, _ in enumerate(video_paths):
35
+ # Scale each video to same height while maintaining aspect ratio
36
+ filter_parts.append(f"[{i}:v]scale=-1:{scale_height}[v{i}]")
37
+ scaled_inputs.append(f"[v{i}]")
38
+
39
+ # Create horizontal stack with gaps
40
+ if num_videos == 1:
41
+ hstack_filter = f"{scaled_inputs[0]}copy[stacked]"
42
+ else:
43
+ # Create colored gap between videos
44
+ gap_filters = []
45
+ for i in range(num_videos - 1):
46
+ gap_filters.append(f"color={gap_color}:{gap_width}x{scale_height}:d=1[gap{i}]")
47
+
48
+ if gap_filters:
49
+ filter_parts.extend(gap_filters)
50
+
51
+ # Build hstack input list with gaps
52
+ hstack_inputs = []
53
+ for i in range(num_videos):
54
+ hstack_inputs.append(scaled_inputs[i])
55
+ if i < num_videos - 1: # Add gap after each video except the last
56
+ hstack_inputs.append(f"[gap{i}]")
57
+
58
+ hstack_filter = f"{''.join(hstack_inputs)}hstack=inputs={len(hstack_inputs)}[stacked]"
59
+
60
+ filter_parts.append(hstack_filter)
61
+
62
+ # Complete the filter complex for stacked video
63
+ stacked_filter = ";".join(filter_parts)
64
+
65
+ # Build FFmpeg command with two-pass palette approach
66
+ cmd = ["ffmpeg", "-y"] # -y to overwrite output file
67
+
68
+ # Add input files
69
+ for video_path in video_paths:
70
+ cmd.extend(["-i", video_path])
71
+
72
+ # Add filter complex and output options
73
+ cmd.extend([
74
+ "-filter_complex", f"{stacked_filter};[stacked]split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3",
75
+ "-r", str(fps), # Set frame rate
76
+ "-loop", "0", # Infinite loop
77
+ output_gif
78
+ ])
79
+
80
+ if verbose:
81
+ print("Running FFmpeg command:")
82
+ print(" ".join(cmd))
83
+ print("\nProcessing...")
84
+
85
+ try:
86
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
87
+ if verbose:
88
+ print(f"✓ Successfully created GIF: {output_gif}")
89
+ return True
90
+ except subprocess.CalledProcessError as e:
91
+ if verbose:
92
+ print(f"✗ FFmpeg error: {e.stderr}")
93
+ return False
94
+ except FileNotFoundError:
95
+ if verbose:
96
+ print("✗ FFmpeg not found. Please install FFmpeg first.")
97
+ print(" - Windows: Download from https://ffmpeg.org/download.html")
98
+ print(" - macOS: brew install ffmpeg")
99
+ print(" - Linux: sudo apt install ffmpeg (Ubuntu/Debian)")
100
+ return False
101
+
102
+ def create_top_to_bottom_gif(video_paths, output_gif, gap_height=20, fps=10, scale_width=320, gap_color="white", verbose=False):
103
+ """
104
+ Create a single GIF with multiple videos stacked vertically (top to bottom).
105
+
106
+ Args:
107
+ video_paths (list): List of paths to input MP4 files
108
+ output_gif (str): Path for output GIF file
109
+ gap_height (int): Height of gap between videos in pixels
110
+ fps (int): Frame rate for output GIF
111
+ scale_width (int): Width to scale all videos to (maintains aspect ratio)
112
+ gap_color (str): Color for gaps between videos (e.g., "white", "black", "red", "#FF0000")
113
+ verbose (bool): Whether to print FFmpeg commands and processing messages
114
+ """
115
+
116
+ if not video_paths:
117
+ raise ValueError("No video paths provided")
118
+
119
+ # Verify all input files exist
120
+ for path in video_paths:
121
+ if not os.path.exists(path):
122
+ raise FileNotFoundError(f"Video file not found: {path}")
123
+
124
+ # Create filter complex string for FFmpeg
125
+ num_videos = len(video_paths)
126
+
127
+ # Input mapping and scaling
128
+ filter_parts = []
129
+ scaled_inputs = []
130
+
131
+ for i, _ in enumerate(video_paths):
132
+ # Scale each video to same width while maintaining aspect ratio
133
+ filter_parts.append(f"[{i}:v]scale={scale_width}:-1[v{i}]")
134
+ scaled_inputs.append(f"[v{i}]")
135
+
136
+ # Create vertical stack with gaps
137
+ if num_videos == 1:
138
+ vstack_filter = f"{scaled_inputs[0]}copy[stacked]"
139
+ else:
140
+ # Create colored gap between videos
141
+ gap_filters = []
142
+ for i in range(num_videos - 1):
143
+ gap_filters.append(f"color={gap_color}:{scale_width}x{gap_height}:d=1[gap{i}]")
144
+
145
+ if gap_filters:
146
+ filter_parts.extend(gap_filters)
147
+
148
+ # Build vstack input list with gaps
149
+ vstack_inputs = []
150
+ for i in range(num_videos):
151
+ vstack_inputs.append(scaled_inputs[i])
152
+ if i < num_videos - 1: # Add gap after each video except the last
153
+ vstack_inputs.append(f"[gap{i}]")
154
+
155
+ vstack_filter = f"{''.join(vstack_inputs)}vstack=inputs={len(vstack_inputs)}[stacked]"
156
+
157
+ filter_parts.append(vstack_filter)
158
+
159
+ # Complete the filter complex for stacked video
160
+ stacked_filter = ";".join(filter_parts)
161
+
162
+ # Build FFmpeg command with two-pass palette approach
163
+ cmd = ["ffmpeg", "-y"] # -y to overwrite output file
164
+
165
+ # Add input files
166
+ for video_path in video_paths:
167
+ cmd.extend(["-i", video_path])
168
+
169
+ # Add filter complex and output options
170
+ cmd.extend([
171
+ "-filter_complex", f"{stacked_filter};[stacked]split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse=dither=bayer:bayer_scale=3",
172
+ "-r", str(fps), # Set frame rate
173
+ "-loop", "0", # Infinite loop
174
+ output_gif
175
+ ])
176
+
177
+ if verbose:
178
+ print("Running FFmpeg command:")
179
+ print(" ".join(cmd))
180
+ print("\nProcessing...")
181
+
182
+ try:
183
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
184
+ if verbose:
185
+ print(f"✓ Successfully created GIF: {output_gif}")
186
+ return True
187
+ except subprocess.CalledProcessError as e:
188
+ if verbose:
189
+ print(f"✗ FFmpeg error: {e.stderr}")
190
+ return False
191
+ except FileNotFoundError:
192
+ if verbose:
193
+ print("✗ FFmpeg not found. Please install FFmpeg first.")
194
+ print(" - Windows: Download from https://ffmpeg.org/download.html")
195
+ print(" - macOS: brew install ffmpeg")
196
+ print(" - Linux: sudo apt install ffmpeg (Ubuntu/Debian)")
197
+ return False
198
+
199
+ def reverse_video(input_video_path, output_filename=None, verbose=False):
200
+ """
201
+ Reverse a video file and save it to /tmp directory.
202
+
203
+ Args:
204
+ input_video_path (str): Path to input MP4 file
205
+ output_filename (str, optional): Name for output file. If None, generates from input filename
206
+ verbose (bool): Whether to print FFmpeg commands and processing messages
207
+
208
+ Returns:
209
+ str: Path to the reversed video file in /tmp, or None if failed
210
+ """
211
+
212
+ if not os.path.exists(input_video_path):
213
+ raise FileNotFoundError(f"Input video file not found: {input_video_path}")
214
+
215
+ # Generate output filename if not provided
216
+ if output_filename is None:
217
+ input_name = Path(input_video_path).stem
218
+ output_filename = f"{input_name}_reversed.mp4"
219
+
220
+ # Ensure output filename has .mp4 extension
221
+ if not output_filename.endswith('.mp4'):
222
+ output_filename += '.mp4'
223
+
224
+ # Create output path in /tmp
225
+ output_path = os.path.join('/tmp', output_filename)
226
+
227
+ # Build FFmpeg command to reverse video
228
+ cmd = [
229
+ "ffmpeg", "-y", # -y to overwrite output file
230
+ "-i", input_video_path,
231
+ "-vf", "reverse", # Video filter to reverse frames
232
+ "-af", "areverse", # Audio filter to reverse audio
233
+ "-c:v", "libx264", # Video codec
234
+ "-c:a", "aac", # Audio codec
235
+ output_path
236
+ ]
237
+
238
+ if verbose:
239
+ print("Running FFmpeg command to reverse video:")
240
+ print(" ".join(cmd))
241
+ print("\nProcessing...")
242
+
243
+ try:
244
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
245
+ if verbose:
246
+ print(f"✓ Successfully created reversed video: {output_path}")
247
+ return output_path
248
+ except subprocess.CalledProcessError as e:
249
+ if verbose:
250
+ print(f"✗ FFmpeg error: {e.stderr}")
251
+ return None
252
+ except FileNotFoundError:
253
+ if verbose:
254
+ print("✗ FFmpeg not found. Please install FFmpeg first.")
255
+ print(" - Windows: Download from https://ffmpeg.org/download.html")
256
+ print(" - macOS: brew install ffmpeg")
257
+ print(" - Linux: sudo apt install ffmpeg (Ubuntu/Debian)")
258
+ return None
259
+
260
+ def add_text_overlay(input_video_path, text, output_filename=None, font_size=12, font_color="white",
261
+ background_color="black", position="top", margin=10, duration=None, verbose=False):
262
+ """
263
+ Add a text overlay to a video with a background title bar.
264
+
265
+ Args:
266
+ input_video_path (str): Path to input MP4 file
267
+ text (str): Text to display
268
+ output_filename (str, optional): Name for output file. If None, generates from input filename
269
+ font_size (int): Font size for the text (default: 24)
270
+ font_color (str): Color of the text (default: "white")
271
+ background_color (str): Color of the background bar (default: "black")
272
+ position (str): Position of text bar - "top", "bottom", "center" (default: "top")
273
+ margin (int): Margin from edge in pixels (default: 10)
274
+ duration (float, optional): Duration to show text in seconds. If None, shows for entire video
275
+ verbose (bool): Whether to print FFmpeg commands and processing messages
276
+
277
+ Returns:
278
+ str: Path to the video with text overlay in /tmp, or None if failed
279
+ """
280
+
281
+ if not os.path.exists(input_video_path):
282
+ raise FileNotFoundError(f"Input video file not found: {input_video_path}")
283
+
284
+ # Generate output filename if not provided
285
+ if output_filename is None:
286
+ input_name = Path(input_video_path).stem
287
+ output_filename = f"{input_name}_with_text.mp4"
288
+
289
+ # Ensure output filename has .mp4 extension
290
+ if not output_filename.endswith('.mp4'):
291
+ output_filename += '.mp4'
292
+
293
+ # Create output path in /tmp
294
+ output_path = os.path.join('/tmp', output_filename)
295
+
296
+ # Determine text position based on position parameter
297
+ if position == "top":
298
+ text_position = f"x={margin}:y={margin+5}" # Add small offset for background box
299
+ elif position == "bottom":
300
+ text_position = f"x={margin}:y=h-th-{margin+5}" # Add small offset for background box
301
+ elif position == "center":
302
+ text_position = f"x={margin}:y=(h-th)/2"
303
+ else:
304
+ text_position = f"x={margin}:y={margin+5}" # Default to top with offset
305
+
306
+ # Build the drawtext filter
307
+ drawtext_filter = f"drawtext=text='{text}':fontsize={font_size}:fontcolor={font_color}:{text_position}"
308
+
309
+ # Add background box if needed
310
+ if background_color != "transparent":
311
+ # Create a semi-transparent background box with estimated height based on font size
312
+ # Estimate text height as approximately 1.2 * font_size
313
+ estimated_text_height = int(font_size * 1.2)
314
+ box_height = estimated_text_height + 10 # Add padding
315
+
316
+ # Position box based on text position
317
+ if position == "top":
318
+ box_y = margin
319
+ elif position == "bottom":
320
+ box_y = f"h-{box_height}-{margin}"
321
+ elif position == "center":
322
+ box_y = f"(h-{box_height})/2"
323
+ else:
324
+ box_y = margin
325
+
326
+ box_filter = f"drawbox=x={margin-5}:y={box_y}:w=iw-{2*(margin-5)}:h={box_height}:color={background_color}@0.7:t=fill"
327
+ drawtext_filter = f"{box_filter},{drawtext_filter}"
328
+
329
+ # Add duration constraint if specified
330
+ if duration is not None:
331
+ drawtext_filter += f":enable='between(t,0,{duration})'"
332
+
333
+ # Build FFmpeg command
334
+ cmd = [
335
+ "ffmpeg", "-y", # -y to overwrite output file
336
+ "-i", input_video_path,
337
+ "-vf", drawtext_filter,
338
+ "-c:v", "libx264", # Video codec
339
+ "-c:a", "copy", # Copy audio without re-encoding
340
+ output_path
341
+ ]
342
+
343
+ if verbose:
344
+ print("Running FFmpeg command to add text overlay:")
345
+ print(" ".join(cmd))
346
+ print("\nProcessing...")
347
+
348
+ try:
349
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
350
+ if verbose:
351
+ print(f"✓ Successfully created video with text overlay: {output_path}")
352
+ return output_path
353
+ except subprocess.CalledProcessError as e:
354
+ if verbose:
355
+ print(f"✗ FFmpeg error: {e.stderr}")
356
+ return None
357
+ except FileNotFoundError:
358
+ if verbose:
359
+ print("✗ FFmpeg not found. Please install FFmpeg first.")
360
+ print(" - Windows: Download from https://ffmpeg.org/download.html")
361
+ print(" - macOS: brew install ffmpeg")
362
+ print(" - Linux: sudo apt install ffmpeg (Ubuntu/Debian)")
363
+ return None
364
+
365
+ def add_text_strip(input_video_path, text, output_filename=None, font_size=16, font_color="white",
366
+ background_color="black", position="top", text_padding=20, max_width_ratio=0.9, verbose=False):
367
+ """
368
+ Add a text strip/bar to a video (increases video height) rather than overlaying text.
369
+
370
+ Args:
371
+ input_video_path (str): Path to input MP4 file
372
+ text (str): Text to display in the strip
373
+ output_filename (str, optional): Name for output file. If None, generates from input filename
374
+ font_size (int): Font size for the text (default: 16)
375
+ font_color (str): Color of the text (default: "white")
376
+ background_color (str): Color of the background strip (default: "black")
377
+ position (str): Position of text strip - "top" or "bottom" (default: "top")
378
+ text_padding (int): Padding around text in pixels (default: 20)
379
+ max_width_ratio (float): Maximum width of text as ratio of video width (default: 0.9)
380
+ verbose (bool): Whether to print FFmpeg commands and processing messages
381
+
382
+ Returns:
383
+ str: Path to the video with text strip in /tmp, or None if failed
384
+ """
385
+
386
+ if not os.path.exists(input_video_path):
387
+ raise FileNotFoundError(f"Input video file not found: {input_video_path}")
388
+
389
+ # Generate output filename if not provided
390
+ if output_filename is None:
391
+ input_name = Path(input_video_path).stem
392
+ output_filename = f"{input_name}_with_strip.mp4"
393
+
394
+ # Ensure output filename has .mp4 extension
395
+ if not output_filename.endswith('.mp4'):
396
+ output_filename += '.mp4'
397
+
398
+ # Create output path in /tmp
399
+ output_path = os.path.join('/tmp', output_filename)
400
+
401
+ # Calculate text strip height based on font size, padding, and estimated line count
402
+ # Estimate characters per line based on font size (roughly 2 characters per font size pixel)
403
+ estimated_chars_per_line = int(font_size * 2)
404
+ text_lines = text.split('\n') if '\n' in text else [text]
405
+
406
+ # If text is too long, wrap it
407
+ wrapped_lines = []
408
+ for line in text_lines:
409
+ if len(line) <= estimated_chars_per_line:
410
+ wrapped_lines.append(line)
411
+ else:
412
+ # Simple word wrapping
413
+ words = line.split(' ')
414
+ current_line = ""
415
+ for word in words:
416
+ if len(current_line + " " + word) <= estimated_chars_per_line:
417
+ current_line += (" " + word) if current_line else word
418
+ else:
419
+ if current_line:
420
+ wrapped_lines.append(current_line)
421
+ current_line = word
422
+ if current_line:
423
+ wrapped_lines.append(current_line)
424
+
425
+ # Calculate strip height based on number of lines
426
+ line_height = font_size + 5 # Add some line spacing
427
+ strip_height = (len(wrapped_lines) * line_height) + (2 * text_padding)
428
+
429
+ # Create text strip using a different approach - pad the video and add text
430
+ # This will add padding above the video and put text in that padded area
431
+ if position == "top":
432
+ # Add padding to top of video and put text in the padded area
433
+ text_strip_filter = f"[0:v]pad=iw:ih+{strip_height}:0:{strip_height}:{background_color}[padded];[padded]drawtext=text='{chr(10).join(wrapped_lines)}':fontsize={font_size}:fontcolor={font_color}:x=(w-tw)/2:y={text_padding}:line_spacing={line_height}[stacked]"
434
+ else: # bottom
435
+ # Add padding to bottom of video and put text in the padded area
436
+ text_strip_filter = f"[0:v]pad=iw:ih+{strip_height}:0:0:{background_color}[padded];[padded]drawtext=text='{chr(10).join(wrapped_lines)}':fontsize={font_size}:fontcolor={font_color}:x=(w-tw)/2:y=h-th-{text_padding}:line_spacing={line_height}[stacked]"
437
+
438
+ # Build FFmpeg command
439
+ cmd = [
440
+ "ffmpeg", "-y", # -y to overwrite output file
441
+ "-i", input_video_path,
442
+ "-filter_complex", text_strip_filter,
443
+ "-map", "[stacked]", # Map the processed video
444
+ "-map", "0:a", # Map the original audio
445
+ "-c:v", "libx264", # Video codec
446
+ "-c:a", "copy", # Copy audio without re-encoding
447
+ output_path
448
+ ]
449
+
450
+ if verbose:
451
+ print("Running FFmpeg command to add text strip:")
452
+ print(" ".join(cmd))
453
+ print("\nProcessing...")
454
+
455
+ try:
456
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
457
+ if verbose:
458
+ print(f"✓ Successfully created video with text strip: {output_path}")
459
+ return output_path
460
+ except subprocess.CalledProcessError as e:
461
+ if verbose:
462
+ print(f"✗ FFmpeg error: {e.stderr}")
463
+ return None
464
+ except FileNotFoundError:
465
+ if verbose:
466
+ print("✗ FFmpeg not found. Please install FFmpeg first.")
467
+ print(" - Windows: Download from https://ffmpeg.org/download.html")
468
+ print(" - macOS: brew install ffmpeg")
469
+ print(" - Linux: sudo apt install ffmpeg (Ubuntu/Debian)")
470
+ return None
471
+
472
+ def get_video_info(video_path):
473
+ """Get basic info about a video file."""
474
+ cmd = [
475
+ "ffprobe", "-v", "quiet", "-print_format", "json",
476
+ "-show_format", "-show_streams", video_path
477
+ ]
478
+
479
+ try:
480
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
481
+ import json
482
+ data = json.loads(result.stdout)
483
+
484
+ # Find video stream
485
+ for stream in data['streams']:
486
+ if stream['codec_type'] == 'video':
487
+ return {
488
+ 'width': stream['width'],
489
+ 'height': stream['height'],
490
+ 'duration': float(stream.get('duration', 0)),
491
+ 'fps': eval(stream.get('r_frame_rate', '0/1'))
492
+ }
493
+ except:
494
+ pass
495
+ return None
496
+
497
+
498
+
499
+
500
+
501
+
502
+ # Example usage
503
+ if __name__ == "__main__":
504
+ # Example video paths - replace with your actual video files
505
+ video_files = [
506
+ "examples/folding_paper.mp4",
507
+ "examples/S008C002P032R002A051.mp4",
508
+ ]
509
+
510
+ output_file = "combined_videos.gif"
511
+
512
+ # Check if example files exist
513
+ existing_files = [f for f in video_files if os.path.exists(f)]
514
+
515
+ if existing_files:
516
+ print(f"Found {len(existing_files)} video files:")
517
+ for video in existing_files:
518
+ info = get_video_info(video)
519
+ if info:
520
+ print(f" {video}: {info['width']}x{info['height']}, {info['duration']:.1f}s")
521
+ else:
522
+ print(f" {video}: (info unavailable)")
523
+
524
+ # Create the horizontal GIF
525
+ success = create_side_by_side_gif(
526
+ video_paths=existing_files,
527
+ output_gif=output_file,
528
+ gap_width=30, # 30px gap between videos
529
+ fps=12, # 12 frames per second
530
+ scale_height=300, # Scale all videos to 300px height
531
+ gap_color="white" # White gap between videos
532
+ )
533
+
534
+ # Also create a vertical GIF
535
+ vertical_output_file = "combined_videos_vertical.gif"
536
+ success_vertical = create_top_to_bottom_gif(
537
+ video_paths=existing_files,
538
+ output_gif=vertical_output_file,
539
+ gap_height=20, # 20px gap between videos
540
+ fps=12, # 12 frames per second
541
+ scale_width=320, # Scale all videos to 320px width
542
+ gap_color="white" # White gap between videos
543
+ )
544
+
545
+ if success:
546
+ file_size = os.path.getsize(output_file) / (1024 * 1024) # MB
547
+ print(f"\nHorizontal GIF size: {file_size:.1f} MB")
548
+
549
+ if success_vertical:
550
+ vertical_file_size = os.path.getsize(vertical_output_file) / (1024 * 1024) # MB
551
+ print(f"Vertical GIF size: {vertical_file_size:.1f} MB")
552
+
553
+ # Example of reversing a video
554
+ if existing_files:
555
+ print(f"\nReversing first video: {existing_files[0]}")
556
+ reversed_path = reverse_video(existing_files[0])
557
+ if reversed_path:
558
+ print(f"Reversed video saved to: {reversed_path}")
559
+
560
+ # Example of adding text overlay
561
+ if existing_files:
562
+ print(f"\nAdding text overlay to first video: {existing_files[0]}")
563
+ text_video_path = add_text_overlay(
564
+ input_video_path=existing_files[0],
565
+ text="Sample Title Text",
566
+ font_size=30,
567
+ font_color="white",
568
+ background_color="black",
569
+ position="top",
570
+ margin=15
571
+ )
572
+ if text_video_path:
573
+ print(f"Video with text overlay saved to: {text_video_path}")
574
+
575
+ # Example of adding text strip
576
+ if existing_files:
577
+ print(f"\nAdding text strip to first video: {existing_files[0]}")
578
+ strip_video_path = add_text_strip(
579
+ input_video_path=existing_files[0],
580
+ text="Video Title Strip",
581
+ font_size=16,
582
+ font_color="white",
583
+ background_color="darkblue",
584
+ position="top",
585
+ text_padding=15
586
+ )
587
+ if strip_video_path:
588
+ print(f"Video with text strip saved to: {strip_video_path}")
589
+ else:
590
+ print("No video files found. Please update the video_files list with your actual MP4 file paths.")
591
+ print("\nExample usage:")
592
+ print("video_files = [")
593
+ print(' "/path/to/your/video1.mp4",')
594
+ print(' "/path/to/your/video2.mp4",')
595
+ print(' "/path/to/your/video3.mp4"')
596
+ print("]")
597
+ print("\n# Create horizontal GIF")
598
+ print("create_side_by_side_gif(video_files, 'horizontal.gif')")
599
+ print("\n# Create vertical GIF")
600
+ print("create_top_to_bottom_gif(video_files, 'vertical.gif')")
601
+ print("\n# Reverse a video")
602
+ print("reversed_path = reverse_video('/path/to/your/video1.mp4')")
603
+ print("print(f'Reversed video: {reversed_path}')")
604
+ print("\n# Add text overlay to video")
605
+ print("text_video = add_text_overlay('/path/to/your/video1.mp4', 'My Title', font_size=30)")
606
+ print("print(f'Video with text: {text_video}')")
607
+ print("\n# Add text strip to video (increases video height)")
608
+ print("strip_video = add_text_strip('/path/to/your/video1.mp4', 'Title Strip', font_size=16)")
609
+ print("print(f'Video with strip: {strip_video}')")
shared/utils/hardware.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import platform
3
+
4
+
5
+ def get_cpu_info_linux():
6
+ try:
7
+ result = subprocess.run(['lscpu'], capture_output=True, text=True, check=True)
8
+ print("---- CPU Info (Linux) ----")
9
+ print(result.stdout)
10
+ except FileNotFoundError:
11
+ print("lscpu command not found.")
12
+ except subprocess.CalledProcessError as e:
13
+ print(f"Error running lscpu: {e}")
14
+
15
+
16
+ def get_gpu_info_nvidia():
17
+ try:
18
+ result = subprocess.run(['nvidia-smi'], capture_output=True, text=True, check=True)
19
+ print("---- GPU Info (NVIDIA) ----")
20
+ print(result.stdout)
21
+ except FileNotFoundError:
22
+ print("nvidia-smi command not found. NVIDIA drivers might not be installed or not in PATH.")
23
+ except subprocess.CalledProcessError as e:
24
+ print(f"Error running nvidia-smi: {e}")
25
+
26
+
27
+ if __name__ == "__main__":
28
+ os_type = platform.system()
29
+ print(f"Operating System: {os_type}")
30
+
31
+ if os_type == "Linux":
32
+ get_cpu_info_linux()
33
+ get_gpu_info_nvidia() # Also try rocm-smi if you have AMD
34
+ elif os_type == "Darwin": # macOS
35
+ print("---- CPU Info (macOS) ----")
36
+ subprocess.run(['sysctl', '-n', 'machdep.cpu.brand_string'])
37
+ subprocess.run(['sysctl', '-n', 'hw.ncpu'])
38
+ # For GPU, check System Information manually or use more specific tools if available
39
+ elif os_type == "Windows":
40
+ print("---- CPU Info (Windows) ----")
41
+ subprocess.run(['wmic', 'cpu', 'get', 'Name,NumberOfCores,NumberOfLogicalProcessors'], shell=True)
42
+ print("---- GPU Info (Windows - NVIDIA Example) ----")
43
+ try:
44
+ subprocess.run(['nvidia-smi'], shell=True) # May need to ensure nvidia-smi is in PATH
45
+ except FileNotFoundError:
46
+ print("nvidia-smi not found. For GPU info, check Task Manager or DxDiag.")
47
+ else:
48
+ print(f"Unsupported OS for this script: {os_type}")
49
+
50
+ # For PyTorch to check CUDA availability and GPU details:
51
+ try:
52
+ import torch
53
+ if torch.cuda.is_available():
54
+ print("\n---- PyTorch CUDA Info ----")
55
+ print(f"CUDA Available: {torch.cuda.is_available()}")
56
+ print(f"CUDA Version (PyTorch compiled with): {torch.version.cuda}")
57
+ print(f"Number of GPUs: {torch.cuda.device_count()}")
58
+ for i in range(torch.cuda.device_count()):
59
+ print(f" GPU {i}: {torch.cuda.get_device_name(i)}")
60
+ print(f" Memory Allocated: {torch.cuda.memory_allocated(i)/1024**2:.2f} MB")
61
+ print(f" Memory Cached: {torch.cuda.memory_reserved(i)/1024**2:.2f} MB") # formerly memory_cached
62
+ props = torch.cuda.get_device_properties(i)
63
+ print(f" Total Memory: {props.total_memory/1024**2:.2f} MB")
64
+ print(f" Compute Capability: {props.major}.{props.minor}")
65
+ else:
66
+ print("\nPyTorch: CUDA is not available.")
67
+ except ImportError:
68
+ print("\nPyTorch is not installed.")
shared/utils/image.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Image operations."""
2
+ from copy import deepcopy
3
+ from PIL import Image
4
+ import numpy as np
5
+
6
+
7
+ def center_crop(im: Image):
8
+ width, height = im.size
9
+ new_width = width if width < height else height
10
+ new_height = height if height < width else width
11
+
12
+ left = (width - new_width)/2
13
+ top = (height - new_height)/2
14
+ right = (width + new_width)/2
15
+ bottom = (height + new_height)/2
16
+
17
+ # Crop the center of the image
18
+ im = im.crop((left, top, right, bottom))
19
+
20
+ return im
21
+
22
+
23
+ def pad_to_square(im: Image, color=(0, 0, 0)):
24
+ im = deepcopy(im)
25
+ width, height = im.size
26
+
27
+ vert_pad = (max(width, height) - height) // 2
28
+ hor_pad = (max(width, height) - width) // 2
29
+
30
+ if len(im.mode) == 3:
31
+ color = (0, 0, 0)
32
+ elif len(im.mode) == 1:
33
+ color = 0
34
+ else:
35
+ raise ValueError(f"Image mode not supported. Image has {im.mode} channels.")
36
+
37
+ return add_margin(im, vert_pad, hor_pad, vert_pad, hor_pad, color=color)
38
+
39
+
40
+ def add_margin(pil_img, top, right, bottom, left, color=(0, 0, 0)):
41
+ """Ref: https://note.nkmk.me/en/python-pillow-add-margin-expand-canvas/"""
42
+ width, height = pil_img.size
43
+ new_width = width + right + left
44
+ new_height = height + top + bottom
45
+ result = Image.new(pil_img.mode, (new_width, new_height), color)
46
+ result.paste(pil_img, (left, top))
47
+ return result
48
+
49
+
50
+ def resize_image(image, new_height, new_width):
51
+ # Convert the numpy array image to PIL Image
52
+ pil_image = Image.fromarray(image)
53
+
54
+ # Resize the PIL Image
55
+ resized_image = pil_image.resize((new_width, new_height))
56
+
57
+ # Convert the resized PIL Image back to numpy array
58
+ resized_image_np = np.array(resized_image)
59
+
60
+ return resized_image_np
61
+
62
+
63
+ def pad_to_width(pil_image, new_width, color=(0, 0, 0)):
64
+ """Pad the image to the specified width."""
65
+ # Convert the numpy array image to PIL Image
66
+ # pil_image = Image.fromarray(image)
67
+
68
+ # Get the current width and height of the image
69
+ width, height = pil_image.size
70
+ assert new_width > width, f"New width {new_width} is less than the current width {width}."
71
+
72
+ # Calculate the padding required
73
+ hor_pad = new_width - width
74
+
75
+ # Add padding to the image
76
+ padded_image = add_margin(pil_image, 0, hor_pad, 0, 0, color=color)
77
+
78
+ # Convert the padded PIL Image back to numpy array
79
+ # padded_image_np = np.array(padded_image)
80
+
81
+ return padded_image
shared/utils/io.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utilities for input-output loading/saving.
3
+ """
4
+
5
+ from typing import Any, List
6
+ import yaml
7
+ import pickle
8
+ import json
9
+ import pandas as pd
10
+
11
+
12
+ class PrettySafeLoader(yaml.SafeLoader):
13
+ """Custom loader for reading YAML files"""
14
+ def construct_python_tuple(self, node):
15
+ return tuple(self.construct_sequence(node))
16
+
17
+
18
+ PrettySafeLoader.add_constructor(
19
+ u'tag:yaml.org,2002:python/tuple',
20
+ PrettySafeLoader.construct_python_tuple
21
+ )
22
+
23
+
24
+ def load_yml(path: str, loader_type: str = 'default'):
25
+ """Read params from a yml file.
26
+
27
+ Args:
28
+ path (str): path to the .yml file
29
+ loader_type (str, optional): type of loader used to load yml files. Defaults to 'default'.
30
+
31
+ Returns:
32
+ Any: object (typically dict) loaded from .yml file
33
+ """
34
+ assert loader_type in ['default', 'safe']
35
+
36
+ loader = yaml.Loader if (loader_type == "default") else PrettySafeLoader
37
+
38
+ with open(path, 'r') as f:
39
+ data = yaml.load(f, Loader=loader)
40
+
41
+ return data
42
+
43
+
44
+ def save_yml(data: dict, path: str):
45
+ """Save params in the given yml file path.
46
+
47
+ Args:
48
+ data (dict): data object to save
49
+ path (str): path to .yml file to be saved
50
+ """
51
+ with open(path, 'w') as f:
52
+ yaml.dump(data, f, default_flow_style=False)
53
+
54
+
55
+ def load_pkl(path: str, encoding: str = "ascii"):
56
+ """Loads a .pkl file.
57
+
58
+ Args:
59
+ path (str): path to the .pkl file
60
+ encoding (str, optional): encoding to use for loading. Defaults to "ascii".
61
+
62
+ Returns:
63
+ Any: unpickled object
64
+ """
65
+ return pickle.load(open(path, "rb"), encoding=encoding)
66
+
67
+
68
+ def save_pkl(data: Any, path: str) -> None:
69
+ """Saves given object into .pkl file
70
+
71
+ Args:
72
+ data (Any): object to be saved
73
+ path (str): path to the location to be saved at
74
+ """
75
+ with open(path, 'wb') as f:
76
+ pickle.dump(data, f)
77
+
78
+
79
+ def load_json(path: str) -> dict:
80
+ """Helper to load json file"""
81
+ with open(path, 'rb') as f:
82
+ data = json.load(f)
83
+ return data
84
+
85
+
86
+ def save_json(data: dict, path: str):
87
+ """Helper to save `dict` as .json file."""
88
+ with open(path, 'w') as f:
89
+ json.dump(data, f, indent=2)
90
+
91
+
92
+ def load_txt(path: str):
93
+ """Loads lines of a .txt file.
94
+
95
+ Args:
96
+ path (str): path to the .txt file
97
+
98
+ Returns:
99
+ List: lines of .txt file
100
+ """
101
+ with open(path) as f:
102
+ lines = f.read().splitlines()
103
+ return lines
104
+
105
+
106
+ def save_txt(data: dict, path: str):
107
+ """Writes data (lines) to a txt file.
108
+
109
+ Args:
110
+ data (dict): List of strings
111
+ path (str): path to .txt file
112
+ """
113
+ assert isinstance(data, list)
114
+
115
+ lines = "\n".join(data)
116
+ with open(path, "w") as f:
117
+ f.write(str(lines))
118
+
119
+
120
+ def read_spreadsheet(sheet_id, gid, url=None, drop_na=True, **kwargs):
121
+ if url is None:
122
+ BASE_URL = 'https://docs.google.com/spreadsheets/d/'
123
+ url = BASE_URL + sheet_id + f'/export?gid={gid}&format=csv'
124
+ df = pd.read_csv(url, **kwargs)
125
+
126
+ if drop_na:
127
+ # drop all rows which have atleast 1 NaN value
128
+ df = df.dropna(axis=0)
129
+
130
+ return df
131
+
132
+
133
+ def load_midi(file, rate=16000):
134
+ import pretty_midi
135
+ assert file.endswith('.mid')
136
+ pm = pretty_midi.PrettyMIDI(file)
137
+ y = pm.synthesize(fs=rate)
138
+ return y, rate
139
+
140
+
141
+ def load_ptz(path):
142
+ import gzip
143
+ import torch
144
+ with gzip.open(path, 'rb') as f:
145
+ data = torch.load(f)
146
+ return data
147
+
148
+
149
+ def save_video(frames, path, fps=30):
150
+ import imageio
151
+ imageio.mimwrite(path, frames, fps=fps)
152
+
153
+
154
+ def read_spreadsheet(sheet_id, gid, gid_key="granularity", **kwargs):
155
+ BASE_URL = 'https://docs.google.com/spreadsheets/d/'
156
+ df = df = pd.read_csv(BASE_URL + sheet_id + f'/export?gid={gid}&format=csv', **kwargs)
157
+ return df
158
+
159
+
160
+ def load_jsonl(file_path: str) -> list:
161
+ """Load data from a JSONL file.
162
+
163
+ Args:
164
+ file_path (str): Path to the JSONL file
165
+
166
+ Returns:
167
+ list: List of dictionaries, where each dictionary is a JSON object from the file
168
+
169
+ Example:
170
+ >>> data = load_jsonl("path/to/file.jsonl")
171
+ >>> print(data[0]) # Print first JSON object
172
+ """
173
+ data = []
174
+ with open(file_path, 'r', encoding='utf-8') as f:
175
+ for line in f:
176
+ if line.strip(): # Skip empty lines
177
+ data.append(json.loads(line))
178
+ return data
179
+
180
+
181
+ def save_jsonl(data: list, file_path: str) -> None:
182
+ """Save data to a JSONL file.
183
+
184
+ Args:
185
+ data (list): List of dictionaries to save
186
+ file_path (str): Path where to save the JSONL file
187
+
188
+ Example:
189
+ >>> data = [{"text": "hello"}, {"text": "world"}]
190
+ >>> save_jsonl(data, "output.jsonl")
191
+ """
192
+ with open(file_path, 'w', encoding='utf-8') as f:
193
+ for item in data:
194
+ f.write(json.dumps(item) + '\n')
shared/utils/keypoint_matching.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Implements keypoint matching for a pair of images."""
2
+ import os
3
+ import numpy as np
4
+ import PIL
5
+ import cv2
6
+ import matplotlib.pyplot as plt
7
+
8
+
9
+ def show_single_image(img, figsize=(7, 5), title="Single image"):
10
+ """Displays a single image."""
11
+ fig = plt.figure(figsize=figsize)
12
+ plt.axis("off")
13
+ plt.imshow(img)
14
+ plt.title(title)
15
+ plt.show()
16
+
17
+
18
+ def show_two_images(img1, img2, title="Two images"):
19
+ """Displays a pair of images."""
20
+ fig, ax = plt.subplots(1, 2, figsize=(10, 5), constrained_layout=True)
21
+
22
+ ax[0].axis("off")
23
+ ax[0].imshow(img1)
24
+
25
+ ax[1].axis("off")
26
+ ax[1].imshow(img2)
27
+
28
+ plt.suptitle(title)
29
+ plt.show()
30
+
31
+
32
+ def show_three_images(img1, img2, img3, ax1_title="", ax2_title="", ax3_title="", title="Three images"):
33
+ """Displays a triplet of images."""
34
+ fig, ax = plt.subplots(1, 3, figsize=(15, 5), constrained_layout=True)
35
+
36
+ ax[0].axis("off")
37
+ ax[0].imshow(img1)
38
+ ax[0].set_title(ax1_title)
39
+
40
+ ax[1].axis("off")
41
+ ax[1].imshow(img2)
42
+ ax[1].set_title(ax2_title)
43
+
44
+ ax[2].axis("off")
45
+ ax[2].imshow(img3)
46
+ ax[2].set_title(ax3_title)
47
+
48
+ plt.suptitle(title)
49
+ plt.show()
50
+
51
+
52
+ class KeypointMatcher:
53
+ """Class for Keypoint matching for a pair of images."""
54
+
55
+ def __init__(self, **sift_args) -> None:
56
+ self.SIFT = cv2.SIFT_create(**sift_args)
57
+ self.BFMatcher = cv2.BFMatcher()
58
+
59
+ @staticmethod
60
+ def _check_images(img1: np.ndarray, img2: np.ndarray):
61
+ assert isinstance(img1, np.ndarray)
62
+ assert len(img1.shape) == 2
63
+
64
+ assert isinstance(img2, np.ndarray)
65
+ assert len(img2.shape) == 2
66
+
67
+ # assert img1.shape == img2.shape
68
+
69
+ @staticmethod
70
+ def _show_matches(img1, kp1, img2, kp2, matches, K=10, figsize=(10, 5), drawMatches_args=dict(matchesThickness=3, singlePointColor=(0, 0, 0))):
71
+ """Displays matches found in the image"""
72
+ selected_matches = np.random.choice(matches, K)
73
+ img3 = cv2.drawMatches(img1, kp1, img2, kp2, selected_matches, outImg=None, **drawMatches_args)
74
+ show_single_image(img3, figsize=figsize, title=f"Randomly selected K = {K} matches between the pair of images.")
75
+ return img3
76
+
77
+ def match(self, img1: PIL.Image, img2: PIL.Image, show_matches: bool = True):
78
+ """Finds, describes and matches keypoints in given pair of images."""
79
+
80
+ img1 = np.array(img1)
81
+ img1 = cv2.cvtColor(img1, cv2.COLOR_RGB2GRAY)
82
+
83
+ img2 = np.array(img2)
84
+ img2 = cv2.cvtColor(img2, cv2.COLOR_RGB2GRAY)
85
+
86
+ # check input images
87
+ self._check_images(img1, img2)
88
+
89
+ # find kps and descriptors in each image
90
+ kp1, des1 = self.SIFT.detectAndCompute(img1, None)
91
+ kp2, des2 = self.SIFT.detectAndCompute(img2, None)
92
+
93
+ # compute matches via Brute-force matching
94
+ matches = self.BFMatcher.match(des1, des2)
95
+
96
+ # sort them in the order of their distance
97
+ matches = sorted(matches, key = lambda x:x.distance)
98
+
99
+ if show_matches:
100
+ self._show_matches(img1, kp1, img2, kp2, matches)
101
+
102
+ return matches, kp1, des1, kp2, des2
103
+
104
+
105
+ def warp(im, M, output_shape):
106
+ out = np.zeros((output_shape[0], output_shape[1]))
107
+ for i in range(output_shape[0]):
108
+ for j in range(output_shape[1]):
109
+ u, v = np.array([[i, j, 0, 0, 1, 0], [0, 0, i, j, 0, 1]]) @ M
110
+ u = int(round(u))
111
+ v = int(round(v))
112
+ if im.shape[0] > u >= 0 and im.shape[1] > v >= 0:
113
+ out[i, j] = im[u, v]
114
+
115
+ return out
116
+
117
+
118
+ def project_2d_to_6d(X: np.ndarray):
119
+ """Projects X (N x 2) to Z (2N x 6) space."""
120
+ N = len(X)
121
+ assert X.shape == (N, 2)
122
+
123
+ Z = np.zeros((2 * N, 6))
124
+ # in columns 0 to 2, fill even indexed rows of Z with X, and fill 5th column with 1
125
+ Z[::2, 0:2] = X
126
+ Z[::2, 4] = 1.0
127
+ # in columns 2 to 4, fill odd indexed rows of Z with X
128
+ Z[1::2, 2:4] = X
129
+ Z[1::2, 5] = 1.0
130
+
131
+ return Z
132
+
133
+
134
+ def project_6d_to_2d(Z: np.ndarray):
135
+ """Projects Z (2N x 6) to X (N x 2) space."""
136
+ N = len(Z) // 2
137
+ assert Z.shape == (2 * N, 6)
138
+
139
+ X_from_even_rows = Z[::2, 0:2]
140
+ X_from_odd_rows = Z[1::2, 2:4]
141
+ assert (X_from_even_rows == X_from_odd_rows).all()
142
+
143
+ return X_from_even_rows
144
+
145
+
146
+
147
+ def project_2d_to_1d(X: np.ndarray):
148
+ """Returns X (N x 2) from Z (2N, 1)"""
149
+ N = len(X)
150
+ X_stretched = np.zeros(2 * N)
151
+ X_stretched[::2] = X[:, 0]
152
+ X_stretched[1::2] = X[:, 1]
153
+ return X_stretched
154
+
155
+
156
+ def project_1d_to_2d(Z: np.ndarray):
157
+ """Returns X (N x 2) from Z (2N, 1)"""
158
+ N = len(Z) // 2
159
+ assert Z.shape == (2 * N,)
160
+
161
+ X = np.zeros((N, 2))
162
+ X[:, 0] = Z[::2]
163
+ X[:, 1] = Z[1::2]
164
+
165
+ return X
166
+
167
+
168
+ def rigid_body_transform(X: np.ndarray, params: np.ndarray):
169
+ """Performs rigid body transformation of points X (N x 2) using params (6 x 1 flattened)"""
170
+ N = len(X)
171
+ assert X.shape == (N, 2)
172
+
173
+ X = project_2d_to_6d(X)
174
+
175
+ X_transformed = np.matmul(X, params)
176
+ X_transformed = project_1d_to_2d(X_transformed)
177
+ assert X_transformed.shape == (N, 2)
178
+
179
+ return X_transformed
180
+
181
+
182
+ def rigid_body_transform_params(X1: np.ndarray, X2: np.ndarray):
183
+ """Returns rigid-body transform parameters RT (6 x 1) assuming transformation between X1 and X2"""
184
+ N = len(X1)
185
+ assert X1.shape == X2.shape
186
+ assert X1.shape == (N, 2)
187
+
188
+ # X2 = X1 * params => params = psuedoinverse(X1) * X2
189
+ X1_expanded = project_2d_to_6d(X1)
190
+ assert X1_expanded.shape == (2 * N, 6)
191
+
192
+ X2_stretched = project_2d_to_1d(X2)
193
+ assert X2_stretched.shape == (2 * N,)
194
+
195
+ params = np.dot(np.linalg.pinv(X1_expanded), X2_stretched)
196
+ return params
197
+
198
+
199
+ class ImageAlignment:
200
+ """Class to perform alignment of a pair of images given keypoints."""
201
+
202
+ def __init__(self) -> None:
203
+ pass
204
+
205
+ @staticmethod
206
+ def show_transformed_points(img1, img2, X1, kp1, kp2, matches, params, num_inliers, num_to_show=20):
207
+ import matplotlib.cm as cm
208
+
209
+ H1, W1 = img1.shape
210
+ H2, W2 = img2.shape
211
+ img = np.hstack([img1, img2])
212
+
213
+ random_matches = np.random.choice(matches, num_to_show)
214
+
215
+ fig, ax = plt.subplots(1, 1, figsize=(15, 6))
216
+ colors = cm.rainbow(np.linspace(0, 1, num_to_show))
217
+
218
+ for i, match in enumerate(random_matches):
219
+
220
+ # select a single match to visualize
221
+ x1, y1 = kp1[match.queryIdx].pt
222
+ x2, y2 = kp2[match.trainIdx].pt
223
+
224
+ # get (x1, y1) transformed to (x1_transformed, y1_transformed)
225
+ A = project_2d_to_6d(np.array([[x1, y1]]))
226
+ (x1_transformed, y1_transformed) = np.dot(A, params)
227
+
228
+ ax.imshow(img, cmap="gray")
229
+ ax.axis("off")
230
+ ax.scatter(x1_transformed + W1, y1_transformed, s=200, marker="x", color=colors[i])
231
+ ax.plot(
232
+ (x1, x1_transformed + W1), (y1, y1_transformed),
233
+ linestyle="--", color=colors[i], marker="o",
234
+ )
235
+
236
+ ax.set_title(
237
+ f"Points in image 1 mapped to transformed points estimated by {num_inliers} points.",
238
+ fontsize=18,
239
+ )
240
+
241
+ os.makedirs("./results/", exist_ok=True)
242
+ plt.savefig(f"./results/match_transformed_inliers_{num_inliers}.png", bbox_inches="tight")
243
+ plt.show()
244
+
245
+ def ransac(
246
+ self, img1, kp1, img2, kp2, matches, num_matches=6, max_iter=500,
247
+ radius_in_px=10, show_transformed=True, inlier_th_for_show=1000
248
+ ):
249
+ """Performs RANSAC to find best matches."""
250
+
251
+ best_inlier_count = 0
252
+ best_params = None
253
+
254
+ # get coordinates of all points in image 1
255
+ X1 = np.array([kp1[matches[i].queryIdx].pt for i in range(len(matches))])
256
+
257
+ # get coordinates of all points in image 2
258
+ X2 = np.array([kp2[matches[i].trainIdx].pt for i in range(len(matches))])
259
+
260
+ for i in range(max_iter):
261
+ # choose matches randomly
262
+ selected_matches = np.random.choice(matches, num_matches)
263
+
264
+ # get matched keypoints in img1
265
+ X1_selected = np.array([kp1[selected_matches[i].queryIdx].pt for i in range(len(selected_matches))])
266
+
267
+ # get matched keypoints in img2
268
+ X2_selected = np.array([kp2[selected_matches[i].trainIdx].pt for i in range(len(selected_matches))])
269
+
270
+ # get transformation parameters
271
+ params = rigid_body_transform_params(X1_selected, X2_selected)
272
+
273
+ # transform X1 to get X2_transformed
274
+ X2_transformed = rigid_body_transform(X1, params)
275
+
276
+ # find inliers
277
+ diff = np.linalg.norm(X2_transformed - X2, axis=1)
278
+ indices = diff < radius_in_px
279
+ num_inliers = sum(indices)
280
+ if num_inliers > best_inlier_count:
281
+ print(f"Found {num_inliers} inliers!")
282
+ best_params = params
283
+ best_inlier_count = num_inliers
284
+
285
+ if show_transformed and num_inliers > inlier_th_for_show:
286
+ self.show_transformed_points(img1, img2, X1, kp1, kp2, matches, best_params, num_inliers)
287
+
288
+ return best_params
289
+
290
+ def align(
291
+ self, img1, kp1, img2, kp2, matches, num_matches=6,
292
+ max_iter=500, show_warped_image=True,
293
+ save_warped=False, path="results/sample.png",
294
+ method="custom"
295
+ ):
296
+ best_params = self.ransac(img1, kp1, img2, kp2, matches, max_iter=max_iter, num_matches=num_matches)
297
+
298
+ # apply the affine transformation using cv2.warpAffine()
299
+ rows, cols = img1.shape[:2]
300
+
301
+ if method == 'custom':
302
+ img1_warped = warp(img1, best_params, (rows, cols))
303
+ else:
304
+ M = np.zeros((2, 3))
305
+ M[0, :2] = best_params[:2]
306
+ M[1, :2] = best_params[2:4]
307
+ M[0, 2] = best_params[4]
308
+ M[1, 2] = best_params[5]
309
+ img1_warped = cv2.warpAffine(img1, M, (cols, rows))
310
+
311
+ if show_warped_image:
312
+ show_three_images(
313
+ img1, img2, img1_warped, title="",
314
+ ax1_title="Image 1", ax2_title="Image 2", ax3_title="Transformation: Image 1 to Image 2",
315
+ )
316
+
317
+ if save_warped:
318
+ plt.imsave(path, img1_warped)
319
+
320
+ return best_params
321
+
322
+
323
+ if __name__ == "__main__":
324
+ # read & show images
325
+ boat1 = cv2.imread('boat1.pgm', cv2.IMREAD_GRAYSCALE)
326
+ boat2 = cv2.imread('boat2.pgm', cv2.IMREAD_GRAYSCALE)
327
+ show_two_images(boat1, boat2, title="Given pair of images.")
328
+
329
+ kp_matcher = KeypointMatcher(contrastThreshold=0.1, edgeThreshold=5)
330
+ matches, kp1, des1, kp2, des2 = kp_matcher.match(boat1, boat2, show_matches=True)