Skip to content

ranges

Functions:

ReplaceRanges

ReplaceRanges(replace_ranges: Callable[P, R])

Class decorator that wraps the [replace_ranges][vstools.utils.replace_ranges] function and extends its functionality.

It is not meant to be used directly.

Methods:

Attributes:

  • exclusive (bool | None) –

    Whether to use exclusive ranges globally (Default: None).

Source code in vstools/functions/ranges.py
177
178
179
def __init__(self, replace_ranges: Callable[P, R]) -> None:
    self._func = replace_ranges
    self.exclusive = None

exclusive instance-attribute

exclusive: bool | None = None

Whether to use exclusive ranges globally (Default: None).

If set to True, all calls of replace_ranges will use exclusive ranges. If set to False, all calls of replace_ranges will use inclusive ranges.

__call__

__call__(
    clip_a: VideoNode,
    clip_b: VideoNode,
    ranges: FrameRangeN | FrameRangesN,
    exclusive: bool | None = None,
    mismatch: Literal[False] = ...,
    planes: Planes = None,
) -> VideoNode
__call__(
    clip_a: VideoNode,
    clip_b: VideoNode,
    ranges: _RangesCallBack,
    *,
    mismatch: bool = False
) -> VideoNode
__call__(
    clip_a: VideoNode,
    clip_b: VideoNode,
    ranges: _RangesCallBackF[VideoFrame] | _RangesCallBackNF[VideoFrame],
    *,
    mismatch: bool = False,
    prop_src: VideoNode
) -> VideoNode
__call__(
    clip_a: VideoNode,
    clip_b: VideoNode,
    ranges: (
        _RangesCallBackF[Sequence[VideoFrame]]
        | _RangesCallBackNF[Sequence[VideoFrame]]
    ),
    *,
    mismatch: bool = False,
    prop_src: Sequence[VideoNode]
) -> VideoNode
__call__(
    clip_a: VideoNode,
    clip_b: VideoNode,
    ranges: FrameRangeN | FrameRangesN | _RangesCallBackLike | None,
    exclusive: bool | None = None,
    mismatch: bool = False,
    planes: Planes = None,
    *,
    prop_src: VideoNode | Sequence[VideoNode] | None = None
) -> VideoNode
__call__(*args: Any, **kwargs: Any) -> Any
Source code in vstools/functions/ranges.py
232
233
def __call__(self, *args: Any, **kwargs: Any) -> Any:
    return self._func(*args, **kwargs)

invert_ranges

invert_ranges(
    clipa: VideoNode,
    clipb: VideoNode | None,
    ranges: FrameRangeN | FrameRangesN,
    exclusive: bool | None = None,
) -> list[tuple[int, int]]

Invert FrameRanges.

Example:

>>> franges = [(100, 200), 600, (1200, 2400)]
>>> invert_ranges(core.std.BlankClip(length=10000), core.std.BlankClip(length=10000), franges)
[(0, 99), (201, 599), (601, 1199), (2401, 9999)]

Parameters:

  • clipa

    (VideoNode) –

    Original clip.

  • clipb

    (VideoNode | None) –

    Replacement clip.

  • ranges

    (FrameRangeN | FrameRangesN) –

    Ranges to replace clipa (original clip) with clipb (replacement clip). These ranges will be inverted. For more info, see replace_ranges.

  • exclusive

    (bool | None, default: None ) –

    Whether to use exclusive (Python-style) ranges. Defaults to False.

Returns:

Source code in vstools/functions/ranges.py
 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
def invert_ranges(
    clipa: vs.VideoNode, clipb: vs.VideoNode | None, ranges: FrameRangeN | FrameRangesN, exclusive: bool | None = None
) -> list[tuple[int, int]]:
    """
    Invert FrameRanges.

    Example:

        >>> franges = [(100, 200), 600, (1200, 2400)]
        >>> invert_ranges(core.std.BlankClip(length=10000), core.std.BlankClip(length=10000), franges)
        [(0, 99), (201, 599), (601, 1199), (2401, 9999)]

    Args:
        clipa: Original clip.
        clipb: Replacement clip.
        ranges: Ranges to replace clipa (original clip) with clipb (replacement clip). These ranges will be inverted.
            For more info, see `replace_ranges`.
        exclusive: Whether to use exclusive (Python-style) ranges. Defaults to False.

    Returns:
        A list of inverted frame ranges.
    """
    return jetp_invert_ranges(
        ranges,
        clipa.num_frames,
        None if clipb is None else clipb.num_frames,
        fallback(exclusive, replace_ranges.exclusive, False),
    )

normalize_franges

normalize_franges(
    ranges: SoftRange, /, exclusive: bool | None = None
) -> Sequence[int]

Normalize ranges represented by a tuple to an iterable of frame numbers.

:param ranges: Ranges to normalize. :param exclusive: Whether to use exclusive (Python-style) ranges. Defaults to False.

:return: List of positive frame ranges.

Source code in vstools/functions/ranges.py
29
30
31
32
33
34
35
36
37
38
39
def normalize_franges(ranges: SoftRange, /, exclusive: bool | None = None) -> Sequence[int]:
    """
    Normalize ranges represented by a tuple to an iterable of frame numbers.

    :param ranges:      Ranges to normalize.
    :param exclusive:   Whether to use exclusive (Python-style) ranges.
                        Defaults to False.

    :return:            List of positive frame ranges.
    """
    return jetp_normalize_range(ranges, fallback(exclusive, replace_ranges.exclusive, False))

normalize_list_to_ranges

normalize_list_to_ranges(
    flist: Iterable[int], min_length: int = 0, exclusive: bool | None = None
) -> list[StrictRange]
Source code in vstools/functions/ranges.py
42
43
44
45
def normalize_list_to_ranges(
    flist: Iterable[int], min_length: int = 0, exclusive: bool | None = None
) -> list[StrictRange]:
    return jetp_normalize_list_to_ranges(flist, min_length, fallback(exclusive, replace_ranges.exclusive, False))

normalize_ranges

normalize_ranges(
    clip: VideoNode,
    ranges: FrameRangeN | FrameRangesN,
    exclusive: bool | None = None,
) -> list[tuple[int, int]]

Normalize ranges to a list of positive ranges.

Frame ranges can include None and negative values. None will be converted to either 0 if it's the first value in a FrameRange, or the clip's length if it's the second item. Negative values will be subtracted from the clip's length.

Examples:

>>> clip.num_frames
1000
>>> normalize_ranges(clip, (None, None))
[(0, 999)]
>>> normalize_ranges(clip, (24, -24))
[(24, 975)]
>>> normalize_ranges(clip, [(24, 100), (80, 150)])
[(24, 150)]

Parameters:

  • clip

    (VideoNode) –

    Input clip.

  • ranges

    (FrameRangeN | FrameRangesN) –

    Frame range or list of frame ranges.

  • exclusive

    (bool | None, default: None ) –

    Whether to use exclusive (Python-style) ranges. Defaults to False.

Returns:

Source code in vstools/functions/ranges.py
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
def normalize_ranges(
    clip: vs.VideoNode, ranges: FrameRangeN | FrameRangesN, exclusive: bool | None = None
) -> list[tuple[int, int]]:
    """
    Normalize ranges to a list of positive ranges.

    Frame ranges can include `None` and negative values.
    None will be converted to either 0 if it's the first value in a FrameRange,
    or the clip's length if it's the second item.
    Negative values will be subtracted from the clip's length.

    Examples:

        >>> clip.num_frames
        1000
        >>> normalize_ranges(clip, (None, None))
        [(0, 999)]
        >>> normalize_ranges(clip, (24, -24))
        [(24, 975)]
        >>> normalize_ranges(clip, [(24, 100), (80, 150)])
        [(24, 150)]

    Args:
        clip: Input clip.
        ranges: Frame range or list of frame ranges.
        exclusive: Whether to use exclusive (Python-style) ranges. Defaults to False.

    Returns:
        List of positive frame ranges.
    """
    return jetp_normalize_ranges(ranges, clip.num_frames, fallback(exclusive, replace_ranges.exclusive, False))

normalize_ranges_to_list

normalize_ranges_to_list(
    ranges: Iterable[SoftRange], exclusive: bool | None = None
) -> list[int]
Source code in vstools/functions/ranges.py
81
82
def normalize_ranges_to_list(ranges: Iterable[SoftRange], exclusive: bool | None = None) -> list[int]:
    return jetp_normalize_ranges_to_list(ranges, fallback(exclusive, replace_ranges.exclusive, False))

remap_frames

remap_frames(
    clip: VideoNode, ranges: Sequence[int | tuple[int, int]]
) -> VideoNode

Remap frames of a clip according to specified ranges.

This function creates a new clip where frames are reordered or repeated based on the given ranges. Ranges can be specified as single frame indices or as ranges of frames.

Example

Remap frames 0, 5, and frames 10 through 15

new_clip = remap_frames(clip, [0, 5, (10, 15)])

This will produce a new clip containing frames: [0, 5, 10, 11, 12, 13, 14, 15] from the original.

Parameters:

  • clip

    (VideoNode) –

    The source clip to remap frames from.

  • ranges

    (Sequence[int | tuple[int, int]]) –

    A sequence of frame indices or tuples representing ranges.

    • If an element is an integer, that frame is included.
    • If an element is a tuple (start, end), all frames from start through end (inclusive, the default, or exclusive through replace_ranges.exclusive) are included.

Returns:

  • VideoNode

    A new clip with frames reordered according to the given ranges.

Source code in vstools/functions/ranges.py
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
def remap_frames(clip: vs.VideoNode, ranges: Sequence[int | tuple[int, int]]) -> vs.VideoNode:
    """
    Remap frames of a clip according to specified ranges.

    This function creates a new clip where frames are reordered or repeated based on the given ranges.
    Ranges can be specified as single frame indices or as ranges of frames.

    Example:
        Remap frames 0, 5, and frames 10 through 15
        ```
        new_clip = remap_frames(clip, [0, 5, (10, 15)])
        ```

        This will produce a new clip containing frames:
        `[0, 5, 10, 11, 12, 13, 14, 15]` from the original.

    Args:
        clip: The source clip to remap frames from.
        ranges: A sequence of frame indices or tuples representing ranges.

               - If an element is an integer, that frame is included.
               - If an element is a tuple ``(start, end)``, all frames from
                ``start`` through ``end`` (inclusive, the default, or exclusive through `replace_ranges.exclusive`)
                are included.

    Returns:
        A new clip with frames reordered according to the given ranges.
    """
    frame_map = list[int](
        flatten(f if isinstance(f, int) else range(f[0], f[1] + (not replace_ranges.exclusive)) for f in ranges)
    )

    base = vs.core.std.BlankClip(clip, length=len(frame_map))

    return vs.core.std.FrameEval(base, lambda n: clip[frame_map[n]], None, clip)

replace_every

replace_every(
    clipa: VideoNode,
    clipb: VideoNode,
    cycle: int,
    offsets: Sequence[int],
    modify_duration: bool = True,
) -> VideoNode

Replace frames in one clip with frames from another at regular intervals.

This function interleaves two clips and then selects frames so that, within each cycle, frames from clipa are replaced with frames from clipb at the specified offsets.

Example

Replace every 3rd frame with a frame from another clip:

new_clip = replace_every(clipa, clipb, cycle=3, offsets=[2])

In this example, within every group of 3 frames: - Frame 0 and 1 come from clipa. - Frame 2 comes from clipb.

Parameters:

  • clipa

    (VideoNode) –

    The base clip to use as the primary source.

  • clipb

    (VideoNode) –

    The replacement clip to take frames from.

  • cycle

    (int) –

    The size of the repeating cycle in frames.

  • offsets

    (Sequence[int]) –

    The positions within each cycle where frames from clipb should replace frames from clipa.

  • modify_duration

    (bool, default: True ) –

    Whether to adjust the clip's duration to reflect inserted frames.

Returns:

  • VideoNode

    A new clip where frames from clipb replace frames in clipa at the specified offsets.

Source code in vstools/functions/ranges.py
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
def replace_every(
    clipa: vs.VideoNode, clipb: vs.VideoNode, cycle: int, offsets: Sequence[int], modify_duration: bool = True
) -> vs.VideoNode:
    """
    Replace frames in one clip with frames from another at regular intervals.

    This function interleaves two clips and then selects frames so that, within each cycle, frames from `clipa`
    are replaced with frames from `clipb` at the specified offsets.

    Example:
        Replace every 3rd frame with a frame from another clip:
        ```
        new_clip = replace_every(clipa, clipb, cycle=3, offsets=[2])
        ```

        In this example, within every group of 3 frames:
        - Frame 0 and 1 come from `clipa`.
        - Frame 2 comes from `clipb`.

    Args:
        clipa: The base clip to use as the primary source.
        clipb: The replacement clip to take frames from.
        cycle: The size of the repeating cycle in frames.
        offsets: The positions within each cycle where frames from `clipb` should replace frames from `clipa`.
        modify_duration: Whether to adjust the clip's duration to reflect inserted frames.

    Returns:
        A new clip where frames from `clipb` replace frames in `clipa` at the specified offsets.
    """
    offsets_a = [x * 2 for x in range(cycle) if x not in offsets]
    offsets_b = [x * 2 + 1 for x in offsets]
    offsets = sorted(offsets_a + offsets_b)

    interleaved = vs.core.std.Interleave([clipa, clipb])

    return vs.core.std.SelectEvery(interleaved, cycle * 2, offsets, modify_duration)

replace_ranges

replace_ranges(
    clip_a: VideoNode,
    clip_b: VideoNode,
    ranges: FrameRangeN | FrameRangesN | _RangesCallBackLike | None,
    exclusive: bool | None = None,
    mismatch: bool = False,
    planes: Planes = None,
    *,
    prop_src: VideoNode | Sequence[VideoNode] | None = None
) -> VideoNode

Replaces frames in a clip, either with pre-calculated indices or on-the-fly with a callback.

Frame ranges are inclusive by default. This behaviour can be changed by setting exclusive=True for one-time use, or set replace_ranges.exclusive = True to apply the change globally.

Examples with clips black and white of equal length:

# Replaces frames 0 and 1 with ``white``
replace_ranges(black, white, [(0, 1)])

# Replaces the entire clip with ``white``
replace_ranges(black, white, [(None, None)])

# Same as previous
replace_ranges(black, white, [(0, None)])

# Replaces 200 until the end with ``white``
replace_ranges(black, white, [(200, None)])

# Replaces 200 until the end with ``white``, leaving 1 frame of ``black``
replace_ranges(black, white, [(200, -1)])

A callback function can be used to replace frames based on frame properties or frame numbers. The function must return a boolean value.

Example of using a callback function
# Replaces frames from ``clip_a`` with ``clip_b`` if the picture type of ``clip_a`` is P.
replace_ranges(clip_a, clip_b, lambda f: get_prop(f, '_PictType', str) == 'P', prop_src=clip_a)``
Optional Dependencies
  • vs-zip (highly recommended!)

Parameters:

  • clip_a

    (VideoNode) –

    Original clip.

  • clip_b

    (VideoNode) –

    Replacement clip.

  • ranges

    (FrameRangeN | FrameRangesN | _RangesCallBackLike | None) –

    Ranges to replace clip_a (original clip) with clip_b (replacement clip).

    Integer values in the list indicate single frames, tuple values indicate inclusive ranges. Callbacks must return true to replace a with b. Negative integer values will be wrapped around based on clip_b's length. None values are context dependent:

    * None provided as sole value to ranges: no-op
    * Single None value in list: Last frame in clip_b
    * None as first value of tuple: 0
    * None as second value of tuple: Last frame in clip_b
    
  • exclusive

    (bool | None, default: None ) –

    Force the use of exclusive (Python-style) ranges.

  • mismatch

    (bool, default: False ) –

    Accept format or resolution mismatch between clips.

  • planes

    (Planes, default: None ) –

    Which planes to process. Only available when vs-zip is installed.

  • prop_src

    (VideoNode | Sequence[VideoNode] | None, default: None ) –

    Source clip(s) to use for frame properties in the callback. This is required if you're using a callback.

Raises:

  • CustomValueError

    If prop_src isn't specified and a callback needs it.

  • CustomValueError

    If a wrong callback signature is provided.

  • CustomValueError

    If planes is specified and vs-zip is not installed..

Returns:

  • VideoNode

    Clip with ranges from clip_a replaced with clip_b.

Source code in vstools/functions/ranges.py
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
@ReplaceRanges
def replace_ranges(
    clip_a: vs.VideoNode,
    clip_b: vs.VideoNode,
    ranges: FrameRangeN | FrameRangesN | _RangesCallBackLike | None,
    exclusive: bool | None = None,
    mismatch: bool = False,
    planes: Planes = None,
    *,
    prop_src: vs.VideoNode | Sequence[vs.VideoNode] | None = None,
) -> vs.VideoNode:
    """
    Replaces frames in a clip, either with pre-calculated indices or on-the-fly with a callback.

    Frame ranges are inclusive by default.
    This behaviour can be changed by setting `exclusive=True` for one-time use,
    or set `replace_ranges.exclusive = True` to apply the change globally.

    Examples with clips ``black`` and ``white`` of equal length:
        ``` py
        # Replaces frames 0 and 1 with ``white``
        replace_ranges(black, white, [(0, 1)])

        # Replaces the entire clip with ``white``
        replace_ranges(black, white, [(None, None)])

        # Same as previous
        replace_ranges(black, white, [(0, None)])

        # Replaces 200 until the end with ``white``
        replace_ranges(black, white, [(200, None)])

        # Replaces 200 until the end with ``white``, leaving 1 frame of ``black``
        replace_ranges(black, white, [(200, -1)])
        ```

    A callback function can be used to replace frames based on frame properties or frame numbers.
    The function must return a boolean value.

    Example of using a callback function:
        ```py
        # Replaces frames from ``clip_a`` with ``clip_b`` if the picture type of ``clip_a`` is P.
        replace_ranges(clip_a, clip_b, lambda f: get_prop(f, '_PictType', str) == 'P', prop_src=clip_a)``
        ```

    Optional Dependencies:
        - [vs-zip](https://github.com/dnjulek/vapoursynth-zip) (highly recommended!)

    Args:
        clip_a: Original clip.
        clip_b: Replacement clip.
        ranges:
            Ranges to replace clip_a (original clip) with clip_b (replacement clip).

            Integer values in the list indicate single frames, tuple values indicate inclusive ranges.
            Callbacks must return true to replace a with b.
            Negative integer values will be wrapped around based on clip_b's length.
            None values are context dependent:

                * None provided as sole value to ranges: no-op
                * Single None value in list: Last frame in clip_b
                * None as first value of tuple: 0
                * None as second value of tuple: Last frame in clip_b

        exclusive: Force the use of exclusive (Python-style) ranges.
        mismatch: Accept format or resolution mismatch between clips.
        planes: Which planes to process. Only available when `vs-zip` is installed.
        prop_src: Source clip(s) to use for frame properties in the callback.
            This is required if you're using a callback.

    Raises:
        CustomValueError: If ``prop_src`` isn't specified and a callback needs it.
        CustomValueError: If a wrong callback signature is provided.
        CustomValueError: If ``planes`` is specified and `vs-zip` is not installed..

    Returns:
        Clip with ranges from clip_a replaced with clip_b.
    """
    if (ranges != 0 and not ranges) or clip_a is clip_b:
        return clip_a

    if not mismatch:
        check_ref_clip(clip_a, clip_b, replace_ranges)

    if callable(ranges):
        from inspect import Signature

        signature = Signature.from_callable(ranges, eval_str=True)

        params = set(signature.parameters.keys())

        base_clip = clip_a.std.BlankClip(keep=True, varformat=mismatch, varsize=mismatch)

        callback = ranges

        if "f" in params and not prop_src:
            raise CustomValueError(
                'To use frame properties in the callback (parameter "f"), '
                "you must specify one or more source clips via `prop_src`!",
                replace_ranges,
            )

        if _is_cb_nf(callback, params):
            return vs.core.std.FrameEval(
                base_clip,
                lambda n, f: clip_b if callback(n, f) else clip_a,
                prop_src,
                [clip_a, clip_b],
            )
        if _is_cb_f(callback, params):
            return vs.core.std.FrameEval(
                base_clip,
                lambda n, f: clip_b if callback(f) else clip_a,
                prop_src,
                [clip_a, clip_b],
            )
        if _is_cb_n(callback, params):
            return vs.core.std.FrameEval(base_clip, lambda n: clip_b if callback(n) else clip_a, None, [clip_a, clip_b])

        raise CustomValueError("Callback must have signature ((n, f) | (n) | (f)) -> bool!", replace_ranges, callback)

    exclusive = fallback(exclusive, replace_ranges.exclusive, False)

    b_ranges = normalize_ranges(clip_b, ranges, exclusive)

    with suppress(AttributeError):
        return vs.core.vszip.RFS(
            clip_a, clip_b, [y for (s, e) in b_ranges for y in range(s, e + (not exclusive))], mismatch, planes
        )

    if planes is not None:
        raise CustomValueError(
            "The `planes` argument is only available when vszip is installed.", replace_ranges, planes
        )

    a_ranges = invert_ranges(clip_a, clip_b, b_ranges, exclusive)

    a_trims = [clip_a[max(0, start - exclusive) : end + (not exclusive)] for start, end in a_ranges]
    b_trims = [clip_b[start : end + (not exclusive)] for start, end in b_ranges]

    if a_ranges:
        main, other = (a_trims, b_trims) if (a_ranges[0][0] == 0) else (b_trims, a_trims)
    else:
        main, other = (b_trims, a_trims) if (b_ranges[0][0] == 0) else (a_trims, b_trims)

    return vs.core.std.Splice(list(interleave_arr(main, other, 1)), mismatch)