Diffusers
Safetensors
File size: 20,987 Bytes
74da989
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
# SPDX-License-Identifier: Apache-2.0
import json
import math
import os
import random
from abc import ABC, abstractmethod
from collections import Counter
from dataclasses import dataclass
from os.path import join as opj
from typing import Any

import numpy as np
import torch
import torchvision
from einops import rearrange
from PIL import Image
from transformers import AutoTokenizer

from trainer.logger import init_logger

logger = init_logger(__name__)


@dataclass
class PreprocessBatch:
    """
    Batch information for dataset processing stages.
    
    This class holds all the information about a video-caption or image-caption pair
    as it moves through the processing pipeline. Fields are populated by different stages.
    """
    # Raw metadata
    path: str
    cap: str | list[str]
    resolution: dict | None = None
    fps: float | None = None
    duration: float | None = None

    # Processed metadata
    num_frames: int | None = None
    sample_frame_index: list[int] | None = None
    sample_num_frames: int | None = None

    # Processed data
    pixel_values: torch.Tensor | None = None
    text: str | None = None
    input_ids: torch.Tensor | None = None
    cond_mask: torch.Tensor | None = None

    @property
    def is_video(self) -> bool:
        """Check if this is a video item."""
        return self.path.endswith(".mp4")

    @property
    def is_image(self) -> bool:
        """Check if this is an image item."""
        return self.path.endswith(".jpg")


class DatasetStage(ABC):
    """
    Abstract base class for dataset processing stages.
    
    Similar to PipelineStage but designed for dataset preprocessing operations.
    """

    @abstractmethod
    def process(self, batch: PreprocessBatch, **kwargs) -> PreprocessBatch:
        """
        Process the dataset batch.
        
        Args:
            batch: Dataset batch to process
            **kwargs: Additional processing parameters
            
        Returns:
            Processed batch
        """
        raise NotImplementedError


class DatasetFilterStage(ABC):
    """
    Abstract base class for dataset filtering stages.
    
    These stages can filter out items during metadata processing.
    """

    @abstractmethod
    def should_keep(self, batch: PreprocessBatch, **kwargs) -> bool:
        """
        Check if batch should be kept.
        
        Args:
            batch: Dataset batch to check
            **kwargs: Additional parameters
            
        Returns:
            True if batch should be kept, False otherwise
        """
        raise NotImplementedError

    @abstractmethod
    def process(self, batch: PreprocessBatch, **kwargs) -> PreprocessBatch:
        """
        Process the dataset batch (for non-filtering operations).
        
        Args:
            batch: Dataset batch to process
            **kwargs: Additional processing parameters
            
        Returns:
            Processed batch
        """
        raise NotImplementedError


class DataValidationStage(DatasetFilterStage):
    """Stage for validating data items."""

    def should_keep(self, batch: PreprocessBatch, **kwargs) -> bool:
        """
        Validate data item.
        
        Args:
            batch: Dataset batch to validate
            
        Returns:
            True if valid, False if invalid
        """
        # Check for caption
        if batch.cap is None:
            return False

        if batch.is_video:
            # Validate video-specific fields
            if batch.duration is None or batch.fps is None:
                return False
        elif not batch.is_image:
            return False

        return True

    def process(self, batch: PreprocessBatch, **kwargs) -> PreprocessBatch:
        """Process does nothing for validation - filtering is handled by should_keep."""
        return batch


class ResolutionFilterStage(DatasetFilterStage):
    """Stage for filtering data items based on resolution constraints."""

    def __init__(self,
                 max_h_div_w_ratio: float = 17 / 16,
                 min_h_div_w_ratio: float = 8 / 16,
                 max_height: int = 1024,
                 max_width: int = 1024):
        self.max_h_div_w_ratio = max_h_div_w_ratio
        self.min_h_div_w_ratio = min_h_div_w_ratio
        self.max_height = max_height
        self.max_width = max_width

    def should_keep(self, batch: PreprocessBatch, **kwargs) -> bool:
        """
        Check if data item passes resolution filtering.
        
        Args:
            batch: Dataset batch with resolution information
            
        Returns:
            True if passes filter, False otherwise
        """
        # Only apply to videos
        if not batch.is_video:
            return True

        if batch.resolution is None:
            return False

        height = batch.resolution.get("height", None)
        width = batch.resolution.get("width", None)
        if height is None or width is None:
            return False

        # Check aspect ratio
        aspect = self.max_height / self.max_width
        hw_aspect_thr = 1.5

        return self.filter_resolution(
            height,
            width,
            max_h_div_w_ratio=hw_aspect_thr * aspect,
            min_h_div_w_ratio=1 / hw_aspect_thr * aspect,
        )

    def process(self, batch: PreprocessBatch, **kwargs) -> PreprocessBatch:
        """Process does nothing for resolution filtering - filtering is handled by should_keep."""
        return batch

    def filter_resolution(self, h: int, w: int, max_h_div_w_ratio: float,
                          min_h_div_w_ratio: float) -> bool:
        """Filter based on height/width ratio."""
        return h / w <= max_h_div_w_ratio and h / w >= min_h_div_w_ratio


class FrameSamplingStage(DatasetFilterStage):
    """Stage for temporal frame sampling and indexing."""

    def __init__(self,
                 num_frames: int,
                 train_fps: int,
                 speed_factor: int = 1,
                 video_length_tolerance_range: float = 5.0,
                 drop_short_ratio: float = 0.0,
                 seed: int = 42):
        self.num_frames = num_frames
        self.train_fps = train_fps
        self.speed_factor = speed_factor
        self.video_length_tolerance_range = video_length_tolerance_range
        self.drop_short_ratio = drop_short_ratio
        # Create a seeded random generator for deterministic sampling
        self.rng = random.Random(seed)

    def should_keep(self, batch: PreprocessBatch, **kwargs) -> bool:
        """
        Check if video should be kept based on length constraints.
        
        Args:
            batch: Dataset batch
            
        Returns:
            True if should be kept, False otherwise
        """
        if batch.is_image:
            return True

        if batch.duration is None or batch.fps is None:
            return False

        num_frames = math.ceil(batch.fps * batch.duration)

        # Check if video is too long
        if (num_frames / batch.fps > self.video_length_tolerance_range *
            (self.num_frames / self.train_fps * self.speed_factor)):
            return False

        # Resample frame indices to check length
        frame_interval = batch.fps / self.train_fps
        start_frame_idx = 0
        frame_indices = np.arange(start_frame_idx, num_frames,
                                  frame_interval).astype(int)

        # Filter short videos
        return not (len(frame_indices) < self.num_frames
                    and self.rng.random() < self.drop_short_ratio)

    def process(self,
                batch: PreprocessBatch,
                temporal_sample_fn=None,
                **kwargs) -> PreprocessBatch:
        """
        Process frame sampling for video data items.
        
        Args:
            batch: Dataset batch
            temporal_sample_fn: Function for temporal sampling
            
        Returns:
            Updated batch with frame sampling info
        """
        if batch.is_image:
            # For images, just add sample info
            batch.sample_frame_index = [0]
            batch.sample_num_frames = 1
            return batch

        assert batch.duration is not None and batch.fps is not None
        batch.num_frames = math.ceil(batch.fps * batch.duration)

        # Resample frame indices
        frame_interval = batch.fps / self.train_fps
        start_frame_idx = 0
        frame_indices = np.arange(start_frame_idx, batch.num_frames,
                                  frame_interval).astype(int)

        # Temporal crop if too long
        if len(frame_indices) > self.num_frames:
            if temporal_sample_fn is not None:
                begin_index, end_index = temporal_sample_fn(len(frame_indices))
                frame_indices = frame_indices[begin_index:end_index]
            else:
                frame_indices = frame_indices[:self.num_frames]

        batch.sample_frame_index = frame_indices.tolist()
        batch.sample_num_frames = len(frame_indices)

        return batch


class VideoTransformStage(DatasetStage):
    """Stage for video data transformation."""

    def __init__(self, transform) -> None:
        self.transform = transform

    def process(self, batch: PreprocessBatch, **kwargs) -> PreprocessBatch:
        """
        Transform video data.
        
        Args:
            batch: Dataset batch with video information
            
        Returns:
            Batch with transformed video tensor
        """
        if not batch.is_video:
            return batch

        assert os.path.exists(batch.path), f"file {batch.path} do not exist!"
        assert batch.sample_frame_index is not None, "Frame indices must be set before transformation"

        torchvision_video, _, metadata = torchvision.io.read_video(
            batch.path, output_format="TCHW")
        video = torchvision_video[batch.sample_frame_index]
        if self.transform is not None:
            video = self.transform(video)
        video = rearrange(video, "t c h w -> c t h w")
        video = video.to(torch.uint8)

        h, w = video.shape[-2:]
        assert (
            h / w <= 17 / 16 and h / w >= 8 / 16
        ), f"Only videos with a ratio (h/w) less than 17/16 and more than 8/16 are supported. But video ({batch.path}) found ratio is {round(h / w, 2)} with the shape of {video.shape}"

        video = video.float() / 127.5 - 1.0
        batch.pixel_values = video
        return batch


class ImageTransformStage(DatasetStage):
    """Stage for image data transformation."""

    def __init__(self, transform, transform_topcrop) -> None:
        self.transform = transform
        self.transform_topcrop = transform_topcrop

    def process(self, batch: PreprocessBatch, **kwargs) -> PreprocessBatch:
        """
        Transform image data.
        
        Args:
            batch: Dataset batch with image information
            
        Returns:
            Batch with transformed image tensor
        """
        if not batch.is_image:
            return batch

        image = Image.open(batch.path).convert("RGB")
        image = torch.from_numpy(np.array(image))
        image = rearrange(image, "h w c -> c h w").unsqueeze(0)

        if self.transform_topcrop is not None:
            image = self.transform_topcrop(image)
        elif self.transform is not None:
            image = self.transform(image)

        image = image.transpose(0, 1)  # [1 C H W] -> [C 1 H W]
        image = image.float() / 127.5 - 1.0
        batch.pixel_values = image
        return batch


class TextEncodingStage(DatasetStage):
    """Stage for text tokenization and encoding."""

    def __init__(self,
                 tokenizer,
                 text_max_length: int,
                 cfg_rate: float = 0.0,
                 seed: int = 42):
        self.tokenizer = tokenizer
        self.text_max_length = text_max_length
        self.cfg_rate = cfg_rate
        # Create a seeded random generator for deterministic CFG
        self.rng = random.Random(seed)

    def process(self, batch: PreprocessBatch, **kwargs) -> PreprocessBatch:
        """
        Process text data.
        
        Args:
            batch: Dataset batch with caption information
            
        Returns:
            Batch with encoded text information
        """
        text = batch.cap
        if not isinstance(text, list):
            text = [text]
        text = [self.rng.choice(text)]

        text = text[0] if self.rng.random() > self.cfg_rate else ""
        text_tokens_and_mask = self.tokenizer(
            text,
            max_length=self.text_max_length,
            padding="max_length",
            truncation=True,
            return_attention_mask=True,
            add_special_tokens=True,
            return_tensors="pt",
        )

        batch.text = text
        batch.input_ids = text_tokens_and_mask["input_ids"]
        batch.cond_mask = text_tokens_and_mask["attention_mask"]
        return batch


class VideoCaptionMergedDataset(torch.utils.data.IterableDataset,
                                torch.distributed.checkpoint.stateful.Stateful):
    """
    Merged dataset for video and caption data with stage-based processing.
    Assumes that data_merge_path is a txt file with the following format:
    <folder_path>,<json_file_path>

        The folder should contain videos.

        The json file should be a list of dictionaries with the following format:
        [
        {
            "path": "1gGQy4nxyUo-Scene-016.mp4",
            "resolution": {
            "width": 1920,
            "height": 1080
            },
            "size": 2439112,
            "fps": 25.0,
            "duration": 6.88,
            "num_frames": 172,
            "cap": [
            "A watermelon wearing a helmet is crushed by a hydraulic press, causing it to flatten and burst open."
            ]
        },
        ...
        ]
    
    This dataset processes video and image data through a series of stages:
    - Data validation
    - Resolution filtering  
    - Frame sampling
    - Transformation
    - Text encoding
    """

    def __init__(self,
                 data_merge_path: str,
                 args,
                 transform,
                 temporal_sample,
                 transform_topcrop,
                 start_idx: int = 0,
                 seed: int = 42):
        self.data_merge_path = data_merge_path
        self.start_idx = start_idx
        self.args = args
        self.temporal_sample = temporal_sample
        self.seed = seed

        # Initialize tokenizer
        tokenizer_path = os.path.join(args.model_path, "tokenizer")
        tokenizer = AutoTokenizer.from_pretrained(tokenizer_path,
                                                  cache_dir=args.cache_dir)

        # Initialize processing stages
        self._init_stages(args, transform, transform_topcrop, tokenizer)

        # Process metadata
        self.processed_batches = self._process_metadata()

    def _init_stages(self, args, transform, transform_topcrop,
                     tokenizer) -> None:
        """Initialize all processing stages."""
        self.validation_stage = DataValidationStage()
        self.resolution_filter_stage = ResolutionFilterStage(
            max_height=args.max_height, max_width=args.max_width)
        self.frame_sampling_stage = FrameSamplingStage(
            num_frames=args.num_frames,
            train_fps=args.train_fps,
            speed_factor=args.speed_factor,
            video_length_tolerance_range=args.video_length_tolerance_range,
            drop_short_ratio=args.drop_short_ratio,
            seed=self.seed)
        self.video_transform_stage = VideoTransformStage(transform)
        self.image_transform_stage = ImageTransformStage(
            transform, transform_topcrop)
        self.text_encoding_stage = TextEncodingStage(
            tokenizer=tokenizer,
            text_max_length=args.text_max_length,
            cfg_rate=args.training_cfg_rate,
            seed=self.seed)

    def _load_raw_data(self) -> list[dict]:
        """Load raw data from JSON files."""
        # Read folder-annotation pairs
        with open(self.data_merge_path) as f:
            folder_anno_pairs = [
                line.strip().split(",") for line in f if line.strip()
            ]
        assert len(
            folder_anno_pairs) == 1, "Only support one folder-annotation pair"
        assert len(folder_anno_pairs[0]
                   ) == 2, "Folder-annotation pair should have two elements"
        folder, annotation_file = folder_anno_pairs[0]

        data_items: list[dict] = []
        with open(annotation_file) as f:
            data_items = json.load(f)

        # Update paths with folder prefix
        for item in data_items:
            item["path"] = opj(folder, item["path"])

        return data_items

    def _process_metadata(self) -> list[PreprocessBatch]:
        """Process the raw metadata through all filtering stages."""
        raw_data = self._load_raw_data()
        processed_batches = []

        # Initialize counters
        filter_counts = {
            "validation_failed": 0,
            "resolution_failed": 0,
            "frame_sampling_failed": 0
        }
        sample_num_frames: list[int] = []

        for item in raw_data:
            batch = PreprocessBatch(path=item["path"],
                                    cap=item["cap"],
                                    resolution=item.get("resolution"),
                                    fps=item.get("fps"),
                                    duration=item.get("duration"))

            # Apply filtering stages
            if not self._apply_filter_stages(batch, filter_counts):
                continue

            # Apply frame sampling processing
            batch = self.frame_sampling_stage.process(
                batch, temporal_sample_fn=self.temporal_sample)

            processed_batches.append(batch)
            assert batch.sample_num_frames is not None
            sample_num_frames.append(batch.sample_num_frames)

        self._log_filtering_stats(filter_counts, sample_num_frames,
                                  len(raw_data), len(processed_batches))
        return processed_batches

    def _apply_filter_stages(self, batch: PreprocessBatch,
                             filter_counts: dict[str, int]) -> bool:
        """Apply all filter stages and update counters. Returns True if batch should be kept."""
        if not self.validation_stage.should_keep(batch):
            filter_counts["validation_failed"] += 1
            return False

        if not self.resolution_filter_stage.should_keep(batch):
            filter_counts["resolution_failed"] += 1
            return False

        if not self.frame_sampling_stage.should_keep(batch):
            filter_counts["frame_sampling_failed"] += 1
            return False

        return True

    def _log_filtering_stats(self, filter_counts: dict[str, int],
                             sample_num_frames: list[int], before_count: int,
                             after_count: int):
        """Log filtering statistics."""
        logger.info(
            "validation_failed: %d, resolution_failed: %d, frame_sampling_failed: %d, "
            "Counter(sample_num_frames): %s, before filter: %d, after filter: %d",
            filter_counts['validation_failed'],
            filter_counts['resolution_failed'],
            filter_counts['frame_sampling_failed'], Counter(sample_num_frames),
            before_count, after_count)

    def __iter__(self):
        """Iterate through processed data items."""
        for idx in range(len(self.processed_batches)):
            yield self._get_item(idx)

    def __len__(self):
        return len(self.processed_batches)

    def _get_item(self, idx: int) -> dict:
        """Get a single processed data item."""
        batch = self.processed_batches[idx]

        # Apply transformation stages
        batch = self.video_transform_stage.process(batch)
        batch = self.image_transform_stage.process(batch)
        batch = self.text_encoding_stage.process(batch)

        # Build result dictionary
        result = {
            "pixel_values": batch.pixel_values,
            "text": batch.text,
            "input_ids": batch.input_ids,
            "cond_mask": batch.cond_mask,
            "path": batch.path,
        }

        # Add video-specific fields
        if batch.is_video:
            result.update({"fps": batch.fps, "duration": batch.duration})

        return result

    def state_dict(self) -> dict[str, Any]:
        """Return state dict for checkpointing."""
        return {"processed_batches": self.processed_batches}

    def load_state_dict(self, state_dict: dict[str, Any]) -> None:
        """Load state dict from checkpoint."""
        self.processed_batches = state_dict["processed_batches"]