CoolFace
Modelpublic

mlx-community/Molmo2-8B-4bit

sourceHugging Faceapache-2.0updated 9mo agoView on Hugging Face
2likes130downloads
video_processing_molmo2.py967 linesDownload Raw Back to root
1"""Video processor class for Molmo2"""2from functools import partial3import os4import warnings5from contextlib import redirect_stdout6from io import BytesIO7from urllib.parse import urlparse8from typing import Optional, Union, Callable9 10import numpy as np11import requests12import einops13import torch14import torchvision.transforms15 16from transformers.image_utils import (17    IMAGENET_STANDARD_MEAN,18    IMAGENET_STANDARD_STD,19    ImageInput,20    PILImageResampling,21    SizeDict,22    validate_kwargs,23)24from transformers.video_utils import (25    VideoInput,26    is_valid_video,27    make_batched_videos,28    make_batched_metadata,29    VideoMetadata,30)31from transformers.processing_utils import Unpack, VideosKwargs32from transformers.video_processing_utils import BaseVideoProcessor33from transformers.utils import logging34from transformers.feature_extraction_utils import BatchFeature35from transformers.utils import (36    is_av_available,37    is_decord_available,38    is_torchcodec_available,39    is_yt_dlp_available,40    TensorType,41    logging,42    to_numpy,43)44 45 46logger = logging.get_logger(__name__)47 48MAX_VIDEO_FPS = 849 50 51def normalize_image(52    image: np.ndarray,53    image_mean: list[float],54    image_std: list[float],55) -> np.ndarray:56    image -= np.array(image_mean, dtype=np.float32)[None, None, :]57    image /= np.array(image_std, dtype=np.float32)[None, None, :]58    return image59 60 61def resize_image(62    image: np.ndarray,63    desired_output_size: list[int],64    resample: PILImageResampling,65) -> np.ndarray:66    if len(image.shape) == 3:67        is_video = False68        image = torch.permute(torch.from_numpy(image), [2, 0, 1])69    else:70        is_video = True71        image = torch.permute(torch.from_numpy(image), [0, 3, 1, 2])72    dtype = image.dtype73    if torch.is_floating_point(image):74        in_min = 0.075        in_max = 1.076        resized = torchvision.transforms.Resize(77            desired_output_size,78            resample,79            antialias=False,80        )(image)81        resized = torch.clip(resized, 0.0, 1.0).to(dtype)82    else:83        assert image.dtype == torch.uint8, "SigLIP expects float images or uint8 images, but got {}".format(image.dtype)84        in_min = 0.085        in_max = 255.086        resized = torchvision.transforms.Resize(87            desired_output_size,88            resample,89            antialias=False,90        )(image)91        resized = torch.clip(resized, 0, 255).to(dtype)92 93    resized = resized.to(torch.float32)94    resized = (resized - in_min) / (in_max - in_min)95 96    if is_video:97        resized = torch.permute(resized, [0, 2, 3, 1]).numpy()98    else:99        resized = torch.permute(resized, [1, 2, 0]).numpy()100 101    return resized102 103 104def build_resized_image(105    image: np.ndarray,106    base_image_input_size: list[int],107    resample: PILImageResampling,108    image_mean: list[float],109    image_std: list[float],110    image_patch_size: int,111) -> tuple[np.ndarray, np.ndarray]:112    resized = resize_image(113        image, base_image_input_size, resample,114    )115    resized = normalize_image(resized, image_mean, image_std)116    if len(resized.shape) == 3:117        resized = np.expand_dims(resized, 0)118    crop_patch_w = base_image_input_size[1] // image_patch_size119    crop_patch_h = base_image_input_size[0] // image_patch_size120    resize_idx = np.arange(crop_patch_w*crop_patch_h).reshape([crop_patch_h, crop_patch_w])121    return resized, resize_idx122 123 124def batch_pixels_to_patches(array: np.ndarray, patch_size: int) -> np.ndarray:125    """Reshape images of [n_images, h, w, 3] -> [n_images, n_patches, pixels_per_patch]"""126    if len(array.shape) == 3:127        n_crops, h, w = array.shape128        h_patches = h//patch_size129        w_patches = w//patch_size130        array = np.reshape(array, [n_crops, h_patches, patch_size, w_patches, patch_size])131        array = np.transpose(array, [0, 1, 3, 2, 4])132        array = np.reshape(array, [n_crops, h_patches*w_patches, patch_size*patch_size])133        return array134    else:135        n_crops, h, w, c = array.shape136        h_patches = h//patch_size137        w_patches = w//patch_size138        array = np.reshape(array, [n_crops, h_patches, patch_size, w_patches, patch_size, c])139        array = np.transpose(array, [0, 1, 3, 2, 4, 5])140        array = np.reshape(array, [n_crops, h_patches*w_patches, patch_size*patch_size*c])141        return array142 143 144def arange_for_pooling(145    idx_arr: np.ndarray,146    pool_h: int,147    pool_w: int,148) -> np.ndarray:149    h_pad = pool_h * ((idx_arr.shape[0] + pool_h - 1) // pool_h) - idx_arr.shape[0]150    w_pad = pool_w * ((idx_arr.shape[1] + pool_w - 1) // pool_w) - idx_arr.shape[1]151    idx_arr = np.pad(idx_arr, [[h_pad//2, (h_pad+1)//2], [w_pad//2, (w_pad+1)//2]],152                     mode='constant',constant_values=-1)153    return einops.rearrange(154        idx_arr, "(h dh) (w dw) -> h w (dh dw)", dh=pool_h, dw=pool_w)155 156 157def image_to_patches_and_grids(158    image: ImageInput,159    base_image_input_size: list[int],160    resample: PILImageResampling,161    image_mean: list[float],162    image_std: list[float],163    image_patch_size: int,164    image_pooling_w: int,165    image_pooling_h: int,166) -> tuple[np.ndarray, np.ndarray, np.ndarray]:167    """168    :return image_grids, the shape of each image after pooling169    :return crops, the image crops to processes with the ViT170    :return pooled_patch_idx, for each patch_id tokens in `image_tokens`, the indices of the171                                patches in `crops` to pool for that token, masked with -1172    """173    if isinstance(base_image_input_size, int):174        base_image_input_size = (base_image_input_size, base_image_input_size)175    176    pooling_w = image_pooling_w177    pooling_h = image_pooling_h178 179    resized, resize_idx = build_resized_image(180        image,181        base_image_input_size,182        resample,183        image_mean,184        image_std,185        image_patch_size,186    )187    pooling_idx = arange_for_pooling(resize_idx, pooling_h, pooling_w)188    h, w = pooling_idx.shape[:2]189    pooling_idx = pooling_idx.reshape([-1, pooling_h*pooling_w])190    image_grid = [h, w]191    return (192        image_grid,193        batch_pixels_to_patches(resized, image_patch_size),194        pooling_idx,195    )196 197 198def get_candidate_target_fps(199    video_fps: Union[int, float],200    sampling_fps: Union[int, float],201    max_fps: Union[int, float] = MAX_VIDEO_FPS,202) -> list[float]:203    """204    Return the subset of `video_fps` factors that remain multiples of `sampling_fps`.205 206    Examples:207        >>> get_candidate_target_fps(video_fps=6, sampling_fps=2)208        [2, 6]209        >>> get_candidate_target_fps(video_fps=5, sampling_fps=1)210        [1, 5]211        >>> get_candidate_target_fps(video_fps=2, sampling_fps=2)212        [2]213        >>> get_candidate_target_fps(video_fps=5, sampling_fps=2)214        Traceback (most recent call last):215            ...216        ValueError: sampling_fps=2 must divide video_fps=5 to produce consistent frame steps.217    """218    video_fps = int(video_fps)219    sampling_fps = int(sampling_fps)220    max_fps = int(max_fps)221 222    if sampling_fps is None:223        raise ValueError("sampling_fps must be provided")224    if video_fps <= 0 or sampling_fps <= 0:225        raise ValueError(f"video_fps and sampling_fps must be positive (got {video_fps}, {sampling_fps})")226    if video_fps % sampling_fps != 0:227        raise ValueError(f"sampling_fps={sampling_fps} must divide video_fps={video_fps}.")228 229    candidates = []230    for candidate in range(sampling_fps, video_fps + 1, sampling_fps):231        if candidate > max_fps:232            break233        if video_fps % candidate == 0:234            candidates.append(float(candidate))235    236    return candidates237 238 239def read_video_decord(240    video_path,241    sample_timestamps_fn: Callable,242    **kwargs,243) -> np.ndarray:244    """245    Decode a video using the Decord backend.246 247    Args:248        video_path (`str`):249            Path to the video file.250        sample_timestamps_fn (`Callable`):251            A callable function that will return timestamps at which the video should be sampled.252 253    Returns:254        tuple[`np.array`, `VideoMetadata`]: A tuple containing:255            - Numpy array of frames in RGB (shape: [num_frames, height, width, 3]).256            - `VideoMetadata` object.257    """258    # Lazy import from decord259    import importlib260    decord = importlib.import_module("decord")261 262    vr = decord.VideoReader(uri=video_path, ctx=decord.cpu(0))  # decord has problems with gpu263    video_fps = vr.get_avg_fps()264    total_num_frames = len(vr)265    time_stamps = vr.get_frame_timestamp(list(range(len(vr))))266    duration = time_stamps[-1][1] - time_stamps[0][0]267 268    metadata = VideoMetadata(269        total_num_frames=int(total_num_frames),270        fps=float(video_fps),271        duration=float(duration),272        video_backend="decord",273    )274 275    target_timestamps = sample_timestamps_fn(metadata=metadata, **kwargs)276    target_timestamps = np.array(target_timestamps)277    offset = time_stamps[0, 0]278 279    ix = np.searchsorted(time_stamps[:, 1], target_timestamps + offset, side='right')280    ix = np.minimum(ix, len(time_stamps) - 1)281 282    video = vr.get_batch(ix).asnumpy()283    metadata.update(284        {285            "frames_indices": target_timestamps * video_fps,286            "height": video.shape[1],287            "width": video.shape[2],288        }289    )290    return video, metadata291 292 293def read_video_torchcodec(294    video_path,295    sample_timestamps_fn: Callable,296    **kwargs,297) -> np.ndarray:298    """299    Decode a video using torchcodec decoder.300 301    Args:302        video_path (`str`):303            Path to the video file.304        sample_timestamps_fn (`Callable`):305            A callable function that will return timestamps at which the video should be sampled.306 307    Returns:308        tuple[`np.array`, `VideoMetadata`]: A tuple containing:309            - Numpy array of frames in RGB (shape: [num_frames, height, width, 3]).310            - `VideoMetadata` object.311    """312    # Lazy import torchcodec313    import importlib314    torchcodec = importlib.import_module("torchcodec")315 316    decoder = torchcodec.decoders.VideoDecoder(317        video_path,318        # Interestingly `exact` mode takes less than approximate when we load the whole video319        seek_mode="exact",320        # Allow FFmpeg decide on the number of threads for efficiency321        num_ffmpeg_threads=0,322    )323    # If the first frame starts at > 0, we effectively clip the video starting at that time324    # since (most) video players would also skip to that time325    time_offset = decoder.metadata.begin_stream_seconds_from_content326    # Note this duration does assume we started playing at `time_offset`327    duration = decoder.metadata.duration_seconds328 329    metadata = VideoMetadata(330        total_num_frames=decoder.metadata.num_frames,331        fps=decoder.metadata.average_fps,332        duration=duration,333        video_backend="torchcodec",334        height=decoder.metadata.height,335        width=decoder.metadata.width,336    )337 338    target_timestamps = sample_timestamps_fn(metadata=metadata, **kwargs)339 340    # Floating point/rounding issues might cause `target_timestamps` to be very slightly341    # out-of-bounds, to handle this we sanity check then clip them342    assert all(x >= 0 for x in target_timestamps)343    assert all(x < duration+1e-6 for x in target_timestamps)344    # 1e-6 padding since torchcodec can throw out-of-bounds errors even if you ask for the345    # exact boundary value, we should still get the first/last frame anyway346    max_timestamp = decoder.metadata.end_stream_seconds_from_content - 1e-6347    min_timestamp = decoder.metadata.begin_stream_seconds_from_content + 1e-6348    # Note we avoid using numpy ops here to reduce floating precision issues349    timestamps = [x + time_offset for x in target_timestamps]350    timestamps = [max(min_timestamp, min(max_timestamp, x)) for x in timestamps]351 352    video = decoder.get_frames_played_at(timestamps).data.numpy().transpose(0, 2, 3, 1)  # Convert to THWC format353    target_timestamps = np.array(target_timestamps)354    metadata.frames_indices = target_timestamps * metadata.fps355 356    return video, metadata357 358 359def read_video_pyav(360    video_path,361    sample_timestamps_fn: Callable,362    **kwargs,363) -> np.ndarray:364    """365    Decode a video using the PyAV backend.366 367    Args:368        video_path (`str`):369            Path to the video file.370        sample_timestamps_fn (`Callable`):371            A callable function that will return timestamps at which the video should be sampled.372 373    Returns:374        tuple[`np.array`, `VideoMetadata`]: A tuple containing:375            - Numpy array of frames in RGB (shape: [num_frames, height, width, 3]).376            - `VideoMetadata` object.377    """378    # Lazy import torchcodec379    import importlib380    av = importlib.import_module("av")381 382    with av.open(video_path) as container:383        video_stream = container.streams.video[0]384        fps = video_stream.average_rate or video_stream.guessed_rate385        it = container.decode(video=0)386        frames = list(it)387 388        stream = container.streams.video[0]389        start = frames[0].pts * stream.time_base390        container_end = stream.duration391        if container_end is not None:392            container_end *= stream.time_base393        if container_end is None or container_end < frames[-1].pts:394            # Some problem with stream duration, so use the frame PTS directly395            # and guess the duration of the last frame396            end = frames[-1].pts * stream.time_base + 1/fps397        else:398            end = container_end399        duration = float(end - start)400 401        metadata = VideoMetadata(402            total_num_frames=len(frames),403            fps=float(fps),404            duration=float(duration),405            video_backend="pyav",406            height=video_stream.height,407            width=video_stream.width,408        )409 410        target_timestamps = sample_timestamps_fn(metadata=metadata, **kwargs)411        offset = float(start)412 413        target_timestamps = np.array(target_timestamps)414        end_time_stamps = np.array([float(frame.pts * stream.time_base) for frame in frames[1:]] + [duration])415        indices = np.searchsorted(end_time_stamps, target_timestamps + offset, side='right')416        indices = np.minimum(indices, len(end_time_stamps) - 1)417 418        video = np.stack(419            [frames[i].to_ndarray(format="rgb24", channel_last=True) for i in indices],420            axis=0,421        )422 423        metadata.frames_indices = target_timestamps * fps424 425        return video, metadata426 427 428VIDEO_DECODERS = {429    "decord": read_video_decord,430    "torchcodec": read_video_torchcodec,431    "pyav": read_video_pyav,432}433 434 435def load_video(436    video: VideoInput,437    backend: str = "decord",438    sample_timestamps_fn: Optional[Callable] = None,439    **kwargs,440):441    """442    Loads `video` to a numpy array.443 444    Args:445        video (`VideoInput`):446            The video to convert to the numpy array format. Can be a link to video or local path.447        backend (`str`, *optional*, defaults to `"decord"`):448            The backend to use when loading the video. Can be any of ["decord", "pyav", ""torchcodec"]. Defaults to "decord".449        sample_timestamps_fn (`Callable`):450            A callable function that will return timestamps at which the video should be sampled.451    """452 453    # Early exit if provided an array or `PIL` frames454    if not isinstance(video, str):455        metadata = [None] * len(video)456        return video, metadata457 458    if urlparse(video).netloc in ["www.youtube.com", "youtube.com"]:459        if not is_yt_dlp_available():460            raise ImportError("To load a video from YouTube url you have  to install `yt_dlp` first.")461        # Lazy import from yt_dlp462        import importlib463        yt_dlp = importlib.import_module("yt_dlp")464 465        buffer = BytesIO()466        with redirect_stdout(buffer), yt_dlp.YoutubeDL() as f:467            f.download([video])468        bytes_obj = buffer.getvalue()469        file_obj = BytesIO(bytes_obj)470    elif video.startswith("http://") or video.startswith("https://"):471        file_obj = BytesIO(requests.get(video).content)472    elif os.path.isfile(video):473        file_obj = video474    else:475        raise TypeError("Incorrect format used for video. Should be an url linking to an video or a local path.")476 477    # can also load with decord, but not cv2/torchvision478    # both will fail in case of url links479    video_is_url = video.startswith("http://") or video.startswith("https://")480    if video_is_url and backend == "opencv":481        raise ValueError("If you are trying to load a video from URL, you cannot use 'opencv' as backend")482 483    if (484        (not is_decord_available() and backend == "decord")485        or (not is_torchcodec_available() and backend == "torchcodec")486        or (not is_av_available() and backend == "pyav")487    ):488        raise ImportError(489            f"You chose backend={backend} for loading the video but the required library is not found in your environment "490            f"Make sure to install {backend} before loading the video."491        )492    493    video_decoder = VIDEO_DECODERS[backend]494    video, metadata = video_decoder(file_obj, sample_timestamps_fn, **kwargs)495    return video, metadata496 497 498def get_target_fps(499    video_fps: float,500    max_frames: int,501    total_frames: int,502    frame_sample_mode: str,503    candidate_target_fps: tuple[float],504) -> float:505    """506    Get the target fps that best spans the video and has the most frames sampled507    """508    num_frames_sampled = 0509    selected_target_fps = None510    for target_fps in candidate_target_fps:511        step_size = max(int(video_fps / target_fps), 1)512        num_frames_sampled_at_fps = int(total_frames / step_size)513        if num_frames_sampled == 0:514            if "uniform" in frame_sample_mode:515                if num_frames_sampled_at_fps > max_frames:516                    break517            selected_target_fps = target_fps518            num_frames_sampled = num_frames_sampled_at_fps519 520        else:521            # the candidate sampling fps increases so frame count can't decrease522            assert num_frames_sampled <= num_frames_sampled_at_fps523            if num_frames_sampled_at_fps > max_frames:524                # choose the sampling fps that spans the video525                continue526 527            elif num_frames_sampled_at_fps > num_frames_sampled:528                # both are less than max_frames, choose the one with higher density of frames sampled529                selected_target_fps = target_fps530                num_frames_sampled = num_frames_sampled_at_fps531    return selected_target_fps532 533 534def get_frame_times_and_chosen_fps(535    selected_target_fps,536    total_frames,537    max_frames,538    video_fps539):540    if selected_target_fps is None:541        frame_indices = np.linspace(0, total_frames, max_frames, endpoint=False, dtype=int)542    else:543        step_size = max(int(video_fps / selected_target_fps), 1)544        frame_indices = np.arange(0, total_frames, step_size)545    if len(frame_indices) > max_frames:546        frame_indices = frame_indices[:max_frames]547    return selected_target_fps, frame_indices548 549 550class Molmo2VideoProcessorKwargs(VideosKwargs, total=False):551    patch_size: Optional[int]552    pooling_size: Optional[list[int]]553    frame_sample_mode: Optional[str]554    max_fps: Optional[int]555    sampling_fps: Optional[int]556 557 558class Molmo2VideoProcessor(BaseVideoProcessor):559    resample = PILImageResampling.BILINEAR560    size = {"height": 378, "width": 378}561    image_mean = IMAGENET_STANDARD_MEAN562    image_std = IMAGENET_STANDARD_STD563    do_resize = True564    do_rescale = True565    do_normalize = True566    do_convert_rgb = True567    patch_size = 14568    pooling_size = [3, 3]569    do_sample_frames = True570    frame_sample_mode = "uniform_last_frame"571    max_fps = 2572    sampling_fps = 2573    valid_kwargs = Molmo2VideoProcessorKwargs574    model_input_names = ["pixel_values_videos", "video_token_pooling", "video_grids"]575 576    def __init__(self, **kwargs: Unpack[Molmo2VideoProcessorKwargs]):577        super().__init__(**kwargs)578        if self.size is not None and (579            self.size.get("height", None) is None or self.size.get("width", None) is None580        ):581            raise ValueError("size must contain 'height' and 'width' keys.")582 583    def _further_process_kwargs(584        self,585        size: Optional[SizeDict] = None,586        **kwargs,587    ) -> dict:588        """589        Update kwargs that need further processing before being validated590        Can be overridden by subclasses to customize the processing of kwargs.591        """592        if size is not None and ("height" not in size or "width" not in size):593            raise ValueError("size must contain 'height' and 'width' keys.")594 595        return super()._further_process_kwargs(size=size, **kwargs)596 597    def sample_times(598        self,599        metadata: VideoMetadata,600        frame_sample_mode: str,601        num_frames: int,602        max_fps: Optional[int] = None,603        sampling_fps: Optional[int] = None,604        **kwargs,605    ) -> np.ndarray:606        """607        Time-based sampling if an array video is passed608        Args:609            metadata (`VideoMetadata`):610                Metadata of the video containing information about total duration, fps and total number of frames.611            frame_sample_mode (`str`, *optional*):612                Mode to sample frames. Defaults to `self.frame_sample_mode`.613            num_frames (`int`, *optional*):614                Maximum number of frames to sample. Defaults to `self.num_frames`.615            man_fps (`int`, *optional*):616                Maximum frames per second to sample.617            sampling_fps (`int`, *optional*):618                Sampling frames per second. Defaults to `self.sampling_fps`.619                Used when `frame_sample_mode` is `"fps"`.620        """621        frame_sample_mode = frame_sample_mode or self.frame_sample_mode622        num_frames = num_frames or self.num_frames623        sampling_fps = sampling_fps or self.sampling_fps624 625        duration = metadata.duration or metadata.total_num_frames / metadata.fps626        if frame_sample_mode == "fps":627            candidate_target_fps = get_candidate_target_fps(metadata.fps, sampling_fps)628            # Try larger and larger FPSs until we hit one that can't span the video629            target_fps = candidate_target_fps[0]630            for candidate_fps in candidate_target_fps[1:]:631                if num_frames / candidate_fps < duration:632                    break633                target_fps = candidate_fps634            times = np.arange(0, num_frames) / target_fps635            times = times[times < duration]636            return times637        elif frame_sample_mode == "uniform_last_frame":638            if max_fps is not None:639                max_duration = (num_frames-1) / max_fps  # -1 to include the last frame640                if max_duration < duration:641                    times = np.linspace(642                        0, duration, num=num_frames, endpoint=True, dtype=np.float64643                    )644                else:645                    times = np.arange(0.0, stop=duration, step=1/max_fps)646                    times = np.concatenate([times, [duration]], axis=0)647                    assert len(times) <= num_frames648            else:649                times = np.linspace(650                    0, duration, num=num_frames, endpoint=True, dtype=np.float64651                )652            return times653        else:654            raise NotImplementedError(frame_sample_mode)655 656    def sample_frames(657        self,658        metadata: VideoMetadata,659        frame_sample_mode: Optional[str] = None,660        num_frames: Optional[int] = None,661        max_fps: Optional[int] = None,662        sampling_fps: Optional[int] = None,663        **kwargs,664    ) -> np.ndarray:665        """666        Frame-based sampling if an array video is passed667        Args:668            metadata (`VideoMetadata`):669                Metadata of the video containing information about total duration, fps and total number of frames.670            frame_sample_mode (`str`, *optional*):671                Mode to sample frames. Defaults to `self.frame_sample_mode`.672            num_frames (`int`, *optional*):673                Maximum number of frames to sample. Defaults to `self.num_frames`.674            max_fps (`int`, *optional*):675                Maximum frames per second to sample.676            sampling_fps (`int`, *optional*):677                Sampling frames per second. Defaults to `self.sampling_fps`.678                Used when `frame_sample_mode` is `"fps"`.679        """680        frame_sample_mode = frame_sample_mode or self.frame_sample_mode681        num_frames = num_frames or self.num_frames682        sampling_fps = sampling_fps or self.sampling_fps683 684        total_num_frames = metadata.total_num_frames685        if frame_sample_mode == "uniform_last_frame" and max_fps is not None:686            duration = total_num_frames / metadata.fps687            if total_num_frames <= 2:688                return np.arange(total_num_frames).astype(int)689            if duration > (num_frames - 1) / max_fps:  # -1 to include the last frame690                # uniform fallback691                indices = np.linspace(692                    0,693                    total_num_frames - 1,694                    num=min(num_frames, total_num_frames),695                    endpoint=True,696                ).astype(int)697                return indices698            else:699                float_indices = np.arange(700                    0.0, stop=total_num_frames - 1, step=float(metadata.fps / max_fps),701                )702                if np.round(float_indices[-1]) != total_num_frames - 1:703                    float_indices = np.concatenate([float_indices, [total_num_frames - 1]], axis=0)704                indices = np.round(float_indices).astype(int)705                assert indices[-1] < total_num_frames706                assert len(float_indices) <= num_frames707                return indices708        elif frame_sample_mode == "uniform_last_frame":709            indices = np.linspace(710                0, total_num_frames - 1, num=min(num_frames, total_num_frames), endpoint=True,711            ).astype(int)712            return indices713        elif frame_sample_mode == "fps":714            candidate_target_fps = get_candidate_target_fps(metadata.fps, sampling_fps)715            selected_target_fps = get_target_fps(716                metadata.fps,717                num_frames,718                total_num_frames,719                frame_sample_mode,720                candidate_target_fps,721            )722            _, indices = get_frame_times_and_chosen_fps(723                selected_target_fps,724                total_num_frames,725                num_frames,726                metadata.fps,727            )728            return indices729        else:730            raise NotImplementedError(frame_sample_mode)731    732    def fetch_videos(733        self,734        video_url_or_urls: Union[str, list[str], list[list[str]]],735        sample_timestamps_fn=None736    ):737        """738        Convert a single or a list of urls into the corresponding `np.array` objects.739 740        If a single url is passed, the return value will be a single object. If a list is passed a list of objects is741        returned.742        """743        if (744            (not is_decord_available())745            and (not is_torchcodec_available())746            and (not is_av_available())747        ):748            raise ImportError(749                "Molmo2VideoProcessor requires `decord`, `torchcodec`, or `av` to be installed."750            )751 752        if is_decord_available():753            backend = "decord"754        elif is_torchcodec_available():755            warnings.warn(756                "`decord` is not installed and cannot be used to decode the video by default. "757                "Falling back to `torchcodec`."758            )759            backend = "torchcodec"760        else:761            warnings.warn(762                "`decord` is not installed and cannot be used to decode the video by default. "763                "Falling back to `PyAV`."764            )765            backend = "pyav"766 767        if isinstance(video_url_or_urls, list):768            return list(zip(*[self.fetch_videos(x, sample_timestamps_fn=sample_timestamps_fn) for x in video_url_or_urls]))769        else:770            return load_video(video_url_or_urls, backend=backend, sample_timestamps_fn=sample_timestamps_fn)771 772    def _decode_and_sample_videos(773        self,774        videos: VideoInput,775        video_metadata: Union[VideoMetadata, dict],776        do_sample_frames: Optional[bool] = None,777        sample_indices_fn: Optional[Callable] = None,778        sample_timestamps_fn: Optional[Callable] = None,779    ):780        """781        Decode input videos and sample frames if needed.782        """783        videos = make_batched_videos(videos)784        video_metadata = make_batched_metadata(videos, video_metadata=video_metadata)785 786        # Framed-based sampling if an array video is passed787        # Otherwise, time-based sampling with decoding788        if is_valid_video(videos[0]) and do_sample_frames:789            assert video_metadata[0].fps is not None, "FPS must be provided for video input"790            sampled_videos = []791            sampled_metadata = []792            for video, metadata in zip(videos, video_metadata):793                indices = sample_indices_fn(metadata=metadata)794                metadata.frames_indices = indices795                sampled_videos.append(video[indices])796                sampled_metadata.append(metadata)797            videos = sampled_videos798            video_metadata = sampled_metadata799        elif not is_valid_video(videos[0]):800            if sample_indices_fn is None:801                logger.warning(802                    "do_sample_frames is False, but video array is not provided: "803                    "Will decode the video and sample frames using Molmo2's default sampling mode"804                )805            if isinstance(videos[0], list):806                raise ValueError(807                    "A list of images is not supported for video input!"808                )809            else:810                videos, video_metadata = self.fetch_videos(videos, sample_timestamps_fn=sample_timestamps_fn)811        812        return videos, video_metadata813    814    def _prepare_input_videos(815        self,816        videos: VideoInput,817        **kwargs,818    ) -> list[np.ndarray]:819        processed_videos = [to_numpy(video) for video in videos]820        return processed_videos821    822    def preprocess(823        self,824        videos: VideoInput,825        **kwargs: Unpack[Molmo2VideoProcessorKwargs],826    ) -> BatchFeature:827        validate_kwargs(828            captured_kwargs=kwargs.keys(),829            valid_processor_keys=list(self.valid_kwargs.__annotations__.keys()) + ["return_tensors"],830        )831 832        # Set default kwargs from self. This ensures that if a kwarg is not provided833        # by the user, it gets its default value from the instance, or is set to None.834        for kwarg_name in self.valid_kwargs.__annotations__:835            kwargs.setdefault(kwarg_name, getattr(self, kwarg_name, None))836        837        do_sample_frames = kwargs.pop("do_sample_frames")838        video_metadata = kwargs.pop("video_metadata")839 840        sample_indices_fn = partial(self.sample_frames, **kwargs) if do_sample_frames else None841        sample_timestamps_fn = partial(self.sample_times, **kwargs)842        videos, video_metadata = self._decode_and_sample_videos(843            videos,844            video_metadata=video_metadata,845            do_sample_frames=do_sample_frames,846            sample_indices_fn=sample_indices_fn,847            sample_timestamps_fn=sample_timestamps_fn,848        )849        videos = self._prepare_input_videos(videos=videos)850 851        kwargs = self._further_process_kwargs(**kwargs)852 853        return_metadata = kwargs.pop("return_metadata")854        preprocessed_videos = self._preprocess(videos=videos, **kwargs)855        if return_metadata:856            preprocessed_videos["video_metadata"] = video_metadata857        return preprocessed_videos858    859    def _preprocess(860        self,861        videos: list[np.ndarray],862        size: Optional[SizeDict] = None,863        resample: Optional[PILImageResampling] = None,864        image_mean: Optional[Union[float, list[float]]] = None,865        image_std: Optional[Union[float, list[float]]] = None,866        do_convert_rgb: Optional[bool] = None,867        patch_size: Optional[int] = None,868        pooling_size: Optional[list[int]] = None,869        return_tensors: Optional[Union[str, TensorType]] = None,870        **kwargs,871    ) -> BatchFeature:872        """873        Preprocess a video for the model.874        Args:875            videos (`VideoInput`):876                Video to preprocess.877            size (`SizeDict`, *optional*, defaults to `self.size`):878                Size of the image after resizing.879            resample (`PILImageResampling`, *optional*, defaults to `self.resample`):880                Resampling filter to use when resizing the image. This can be one of the enum `PILImageResampling`. Only881                has an effect if `do_resize` is set to `True`.882            image_mean (`float` or `list[float]`, *optional*, defaults to `self.image_mean`):883                Image mean to use for normalization. Only has an effect if `do_normalize` is set to `True`.884            image_std (`float` or `list[float]`, *optional*, defaults to `self.image_std`):885                Image standard deviation to use for normalization. Only has an effect if `do_normalize` is set to886                `True`.887            do_convert_rgb (`bool`, *optional*, defaults to `self.do_convert_rgb`):888                Whether to convert the image to RGB.889            patch_size (`int`, *optional*, defaults to `self.patch_size`):890                The spatial patch size of the vision encoder.891            pooling_size (`list[int]`, *optional*, defaults to `self.pooling_size`):892                The pooling size of the vision adapter.893            return_tensors (`str` or `TensorType`, *optional*):894                The type of tensors to return. Can be one of:895                - Unset: Return a list of `np.ndarray`.896                - `TensorType.TENSORFLOW` or `'tf'`: Return a batch of type `tf.Tensor`.897                - `TensorType.PYTORCH` or `'pt'`: Return a batch of type `torch.Tensor`.898                - `TensorType.NUMPY` or `'np'`: Return a batch of type `np.ndarray`.899                - `TensorType.JAX` or `'jax'`: Return a batch of type `jax.numpy.ndarray`.900 901        Returns:902            A `BatchFeature` containing the following keys:903                - `pixel_values_videos`: The preprocessed videos.904                - `video_token_pooling`: The indices of the patches in `crops` to pool for each token in `video_tokens`.905                - `video_grids`: The video grids.906        """907        if size.height is None or size.width is None:908            raise ValueError("size must contain 'height' and 'width' keys.")909        910        base_image_input_size = [size.height, size.width]911 912        resample = resample or self.resample913        image_mean = image_mean or self.image_mean914        image_std = image_std or self.image_std915        do_convert_rgb = do_convert_rgb or self.do_convert_rgb916 917        patch_size = patch_size or self.patch_size918        pooling_size = pooling_size or self.pooling_size919 920        image_pooling_h, image_pooling_w = pooling_size921 922        batch_grids = []923        batch_crops = []924        batch_pooled_patches_idx = []925 926        for video in videos:927            all_crops = []928            pooled_patches_idx = []929 930            for frame in video:931                image_grid, crops, pooled_idx = image_to_patches_and_grids(932                    frame,933                    base_image_input_size,934                    resample,935                    image_mean,936                    image_std,937                    patch_size,938                    image_pooling_w,939                    image_pooling_h,940                )941                offset = sum(np.prod(x.shape[:2]) for x in all_crops)942                pooled_idx_with_offset = np.where(pooled_idx >= 0, pooled_idx + offset, pooled_idx)943                pooled_patches_idx.append(pooled_idx_with_offset)944                all_crops.append(crops)945 946            video_grid = np.array([len(video), image_grid[0], image_grid[1]])947            all_crops = np.concatenate(all_crops, 0)948            pooled_patches_idx = np.concatenate(pooled_patches_idx, 0)949 950            batch_grids.append(video_grid)951            batch_crops.append(all_crops)952            batch_pooled_patches_idx.append(pooled_patches_idx)953        954        video_grids = np.stack(batch_grids, 0)955        pixel_values_videos = np.concatenate(batch_crops, 0)956        video_token_pooling = np.concatenate(batch_pooled_patches_idx, 0)957        958        data =dict(959            pixel_values_videos=pixel_values_videos,960            video_token_pooling=video_token_pooling,961            video_grids=video_grids,962        )963 964        return BatchFeature(data, tensor_type=return_tensors)965 966 967Molmo2VideoProcessor.register_for_auto_class()