File size: 9,971 Bytes
a1833c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: OpenMDW-1.1

import math
import re
from typing import List, Tuple

IMAGE_RES_SIZE_INFO: dict[str, dict[str, tuple[int, int]]] = {
    # Our desired 256 resolution is the one below (commented).
    # Desired: "256": {"1,1": (336, 336), "4,3": (384, 288), "3,4": (288, 384), "16,9": (448, 256), "9,16": (256, 448)},
    "256": {
        "1,1": (256, 256),
        "4,3": (320, 256),
        "3,4": (256, 320),
        "16,9": (320, 192),
        "9,16": (192, 320),
    },
    "480": {"1,1": (640, 640), "4,3": (736, 544), "3,4": (544, 736), "16,9": (832, 480), "9,16": (480, 832)},
    # 704 resolutions are nicely divisible by 32
    "704": {"1,1": (960, 960), "4,3": (1088, 832), "3,4": (832, 1088), "16,9": (1280, 704), "9,16": (704, 1280)},
    "720": {"1,1": (960, 960), "4,3": (1104, 832), "3,4": (832, 1104), "16,9": (1280, 720), "9,16": (720, 1280)},
    # 768 for arena.ai
    "768": {"1,1": (1024, 1024), "4,3": (1184, 880), "3,4": (880, 1184), "16,9": (1360, 768), "9,16": (768, 1360)},
    "1080": {"1,1": (1440, 1440), "4,3": (1664, 1248), "3,4": (1248, 1664), "16,9": (1920, 1080), "9,16": (1080, 1920)},
    "1280": {"1,1": (1712, 1712), "4,3": (1968, 1472), "3,4": (1472, 1968), "16,9": (2272, 1280), "9,16": (1280, 2272)},
    "2048": {
        "1,1": (2728, 2728),
        "4,3": (3160, 2368),
        "3,4": (2368, 3160),
        "16,9": (3640, 2048),
        "9,16": (2048, 3640),
    },
    "gt_2048": {
        "1,1": (5464, 5464),
        "4,3": (6304, 4728),
        "3,4": (4728, 6304),
        "16,9": (7280, 4096),
        "9,16": (4096, 7280),
    },
}

VIDEO_RES_SIZE_INFO: dict[str, dict[str, tuple[int, int]]] = {
    # Our desired 256 resolution is the one below (commented).
    # Desired: "256": {"1,1": (336, 336), "4,3": (384, 288), "3,4": (288, 384), "16,9": (448, 256), "9,16": (256, 448)},
    "256": {
        "1,1": (256, 256),
        "4,3": (320, 256),
        "3,4": (256, 320),
        "16,9": (320, 192),
        "9,16": (192, 320),
    },
    # "2,3" (h/w=1.5, W:H=2:3): added for the BEHAVIOR-1K R1Pro concat_view composite
    # (native 1080x720), so it snaps to an exact-aspect 512x768 canvas with no
    # reflection padding. Scoped to the 480 tier that b1k trains at.
    "480": {
        "1,1": (640, 640),
        "4,3": (736, 544),
        "3,4": (544, 736),
        "16,9": (832, 480),
        "9,16": (480, 832),
        "2,3": (512, 768),
    },
    # 704 resolutions are nicely divisible by 32
    "704": {"1,1": (960, 960), "4,3": (1088, 832), "3,4": (832, 1088), "16,9": (1280, 704), "9,16": (704, 1280)},
    "720": {"1,1": (960, 960), "4,3": (1104, 832), "3,4": (832, 1104), "16,9": (1280, 720), "9,16": (720, 1280)},
    # 768 for arena.ai
    "768": {"1,1": (1024, 1024), "4,3": (1184, 880), "3,4": (880, 1184), "16,9": (1360, 768), "9,16": (768, 1360)},
    "1080": {"1,1": (1440, 1440), "4,3": (1664, 1248), "3,4": (1248, 1664), "16,9": (1920, 1080), "9,16": (1080, 1920)},
    "1280": {"1,1": (1712, 1712), "4,3": (1968, 1472), "3,4": (1472, 1968), "16,9": (2272, 1280), "9,16": (1280, 2272)},
    "2048": {
        "1,1": (2728, 2728),
        "4,3": (3160, 2368),
        "3,4": (2368, 3160),
        "16,9": (3640, 2048),
        "9,16": (2048, 3640),
    },
    "gt_2048": {
        "1,1": (5464, 5464),
        "4,3": (6304, 4728),
        "3,4": (4728, 6304),
        "16,9": (7280, 4096),
        "9,16": (4096, 7280),
    },
}


def get_aspect_ratios_from_wdinfos(wdinfos: list[str]) -> list[str]:
    aspect_ratios = []
    for wdinfo in wdinfos:
        aspect_ratio_match = re.search(r"aspect_ratio_(\d+_\d+)", wdinfo)
        aspect_ratios.append(aspect_ratio_match.group(1))

    return aspect_ratios


def get_wdinfos_w_aspect_ratio(wdinfos: list[str]) -> List[Tuple[str, str]]:
    aspect_ratios = get_aspect_ratios_from_wdinfos(wdinfos)

    # return a list of (wdinfo_path, aspect_ratio) pairs
    return [(wdinfo, aspect_ratio.replace("_", ",")) for wdinfo, aspect_ratio in zip(wdinfos, aspect_ratios)]


def parse_frame_range_from_wdinfo(wdinfo: str) -> tuple[int, int | float] | None:
    """
    Parse frame range from wdinfo path.

    Args:
        wdinfo: wdinfo path string containing a frames_X_Y pattern, where Y may be ``inf``

    Returns:
        Tuple of (min_frames, max_frames) if found, where max_frames is
        ``math.inf`` for unbounded ``frames_X_inf`` and ``frames_gt_X`` buckets.
        ``frames_gt_X`` is treated as starting at X. Returns None if no frame
        range is present.

    Example:
        >>> parse_frame_range_from_wdinfo("wdinfo/v4/tv_drama/resolution_720/aspect_ratio_16_9/frames_300_400/wdinfo.json")
        (300, 400)
        >>> parse_frame_range_from_wdinfo("wdinfo/v4/tv_drama/resolution_720/aspect_ratio_16_9/frames_3700_inf/wdinfo.json")
        (3700, inf)
    """
    match = re.search(r"frames_(\d+)_(\d+|inf)(?!\w)", wdinfo)
    if match:
        max_frames = math.inf if match.group(2) == "inf" else int(match.group(2))
        return (int(match.group(1)), max_frames)
    match = re.search(r"frames_gt_(\d+)", wdinfo)
    if match:
        # Wdinfo generation uses frames_gt_X for the bucket that starts at X, so treat it as >= X.
        return (int(match.group(1)), math.inf)
    return None


def normalize_skip_frame_ranges(
    skip_frame_range: str | list[str] | None,
) -> set[tuple[int, int | float]]:
    """Normalize ``skip_frame_range`` into a set of (min_frames, max_frames) buckets.

    Args:
        skip_frame_range: A single bucket string like ``"300_400"`` or ``"3700_inf"``,
            a list of such strings, or None. Each string identifies the frame-range
            bucket (e.g. ``frames_300_400``) that should be skipped.

    Returns:
        Set of (min_frames, max_frames) tuples to skip. An ``inf`` upper bound is
        represented by ``math.inf``. Empty if ``skip_frame_range`` is None.
    """
    if skip_frame_range is None:
        return set()

    if isinstance(skip_frame_range, str):
        skip_frame_range = [skip_frame_range]

    skip_buckets: set[tuple[int, int | float]] = set()
    for bucket in skip_frame_range:
        match = re.fullmatch(r"(\d+)_(\d+|inf)", bucket.strip())
        if match is None:
            raise ValueError(
                f"Invalid skip_frame_range entry {bucket!r}. "
                "Expected the form '<min>_<max>', e.g. '300_400' or '3700_inf'."
            )
        max_frames = math.inf if match.group(2) == "inf" else int(match.group(2))
        skip_buckets.add((int(match.group(1)), max_frames))

    return skip_buckets


def filter_wdinfos_by_frame_range(
    wdinfos: list[str],
    min_frames: int | None = None,
    max_frames: int | None = None,
    skip_frame_range: str | list[str] | None = None,
) -> list[str]:
    """
    Filter wdinfo files based on frame range.

    The frame range in wdinfo path (e.g., frames_300_400) represents videos
    with frames between those values. This function filters wdinfo files
    based on the wdinfo's upper bound (wdinfo_max):
    - min_frames is EXCLUSIVE: wdinfo_max must be > min_frames
    - max_frames is INCLUSIVE: wdinfo_max must be <= max_frames
    - an ``inf`` upper bound is treated as infinity and excluded by any finite max_frames

    Additionally, any wdinfo whose frame-range bucket matches an entry in
    ``skip_frame_range`` is excluded.

    Args:
        wdinfos: List of wdinfo paths
        min_frames: Minimum number of frames (exclusive). If None, no lower bound.
        max_frames: Maximum number of frames (inclusive). If None, no upper bound.
        skip_frame_range: Frame-range bucket(s) to exclude, e.g. ``"300_400"`` to
            drop the ``frames_300_400`` bucket or ``"3700_inf"`` to drop the
            ``frames_3700_inf`` bucket. Accepts a single string or a list of strings.
            If None, no bucket is skipped.

    Returns:
        Filtered list of wdinfo paths

    Example:
        >>> wdinfos = [
        ...     "wdinfo/frames_400_500/wdinfo.json",
        ...     "wdinfo/frames_500_600/wdinfo.json",
        ...     "wdinfo/frames_600_700/wdinfo.json",
        ... ]
        >>> filter_wdinfos_by_frame_range(wdinfos, min_frames=500, max_frames=600)
        ['wdinfo/frames_500_600/wdinfo.json']
        # frames_400_500 excluded because wdinfo_max (500) <= min_frames (500)
        # frames_500_600 included because wdinfo_max (600) > min_frames (500) AND <= max_frames (600)
        # frames_600_700 excluded because wdinfo_max (700) > max_frames (600)

        >>> filter_wdinfos_by_frame_range(wdinfos, skip_frame_range="500_600")
        ['wdinfo/frames_400_500/wdinfo.json', 'wdinfo/frames_600_700/wdinfo.json']
        # frames_500_600 excluded because its bucket matches skip_frame_range
    """
    skip_buckets = normalize_skip_frame_ranges(skip_frame_range)

    if min_frames is None and max_frames is None and not skip_buckets:
        return wdinfos

    filtered = []
    for wdinfo in wdinfos:
        frame_range = parse_frame_range_from_wdinfo(wdinfo)
        if frame_range is None:
            # If no frame range in path, include by default
            filtered.append(wdinfo)
            continue

        wdinfo_min, wdinfo_max = frame_range

        # Skip explicitly excluded buckets (matched on the full (min, max) bucket).
        if (wdinfo_min, wdinfo_max) in skip_buckets:
            continue

        # Filter based on wdinfo's upper bound (wdinfo_max):
        # - min_frames is exclusive: wdinfo_max must be > min_frames
        # - max_frames is inclusive: wdinfo_max must be <= max_frames
        include = True
        if min_frames is not None and wdinfo_max <= min_frames:
            include = False
        if max_frames is not None and wdinfo_max > max_frames:
            include = False

        if include:
            filtered.append(wdinfo)

    return filtered