• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Jaded-Encoding-Thaumaturgy / vs-tools / 13117979512

03 Feb 2025 04:21PM UTC coverage: 58.155% (-0.007%) from 58.162%
13117979512

Pull #171

github

web-flow
Merge 598b737da into cca93572c
Pull Request #171: Use `with x.get_frame` instead of `x.get_frame` globally

5 of 13 new or added lines in 7 files covered. (38.46%)

29 existing lines in 1 file now uncovered.

2831 of 4868 relevant lines covered (58.16%)

0.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

30.56
/vstools/utils/ranges.py
1
from __future__ import annotations
1✔
2

3
from typing import Callable, Sequence, Union, overload
1✔
4

5
import vapoursynth as vs
1✔
6
from stgpytools import CustomValueError, flatten, interleave_arr, ranges_product
1✔
7

8
from ..functions import check_ref_clip
1✔
9
from ..types import FrameRangeN, FrameRangesN
1✔
10

11
__all__ = [
1✔
12
    'replace_ranges',
13

14
    'remap_frames',
15

16
    'replace_every',
17

18
    'ranges_product',
19

20
    'interleave_arr',
21
]
22

23

24
_gc_func_gigacope = []
1✔
25

26
RangesCallback = Union[
1✔
27
    Callable[[int], bool],
28
    Callable[[vs.VideoFrame], bool],
29
    Callable[[list[vs.VideoFrame]], bool],
30
    Callable[[vs.VideoFrame | list[vs.VideoFrame]], bool],
31
    Callable[[int, vs.VideoFrame], bool],
32
    Callable[[int, list[vs.VideoFrame]], bool],
33
    Callable[[int, vs.VideoFrame | list[vs.VideoFrame]], bool]
34
]
35

36

37
@overload
1✔
38
def replace_ranges(
1✔
39
    clip_a: vs.VideoNode, clip_b: vs.VideoNode,
40
    ranges: FrameRangeN | FrameRangesN | Callable[[vs.VideoFrame], bool] | Callable[[int, vs.VideoFrame], bool] | None,
41
    exclusive: bool = False, mismatch: bool = False,
42
    *, prop_src: vs.VideoNode
43
) -> vs.VideoNode:
44
    ...
×
45

46

47
@overload
1✔
48
def replace_ranges(
1✔
49
    clip_a: vs.VideoNode, clip_b: vs.VideoNode, ranges: FrameRangeN | FrameRangesN | Callable[
50
        [list[vs.VideoFrame]], bool
51
    ] | Callable[[int, list[vs.VideoFrame]], bool] | None, exclusive: bool = False,
52
    mismatch: bool = False,
53
    *, prop_src: list[vs.VideoNode]
54
) -> vs.VideoNode:
55
    ...
×
56

57

58
@overload
1✔
59
def replace_ranges(
1✔
60
    clip_a: vs.VideoNode, clip_b: vs.VideoNode, ranges: FrameRangeN | FrameRangesN | Callable[
61
        [vs.VideoFrame | list[vs.VideoFrame]], bool
62
    ] | Callable[[int, vs.VideoFrame | list[vs.VideoFrame]], bool] | None, exclusive: bool = False,
63
    mismatch: bool = False, *, prop_src: vs.VideoNode | list[vs.VideoNode] | None = None
64
) -> vs.VideoNode:
65
    ...
×
66

67

68
@overload
1✔
69
def replace_ranges(
1✔
70
    clip_a: vs.VideoNode, clip_b: vs.VideoNode,
71
    ranges: FrameRangeN | FrameRangesN | Callable[[int], bool] | None,
72
    exclusive: bool = False, mismatch: bool = False, *, prop_src: None = None
73
) -> vs.VideoNode:
74
    ...
×
75

76

77
@overload
1✔
78
def replace_ranges(
1✔
79
    clip_a: vs.VideoNode, clip_b: vs.VideoNode,
80
    ranges: FrameRangeN | FrameRangesN | RangesCallback | None,
81
    exclusive: bool = False, mismatch: bool = False,
82
    *, prop_src: vs.VideoNode | list[vs.VideoNode] | None = None
83
) -> vs.VideoNode:
84
    ...
×
85

86

87
def replace_ranges(
1✔
88
    clip_a: vs.VideoNode, clip_b: vs.VideoNode,
89
    ranges: FrameRangeN | FrameRangesN | RangesCallback | None,
90
    exclusive: bool = False, mismatch: bool = False,
91
    *, prop_src: vs.VideoNode | list[vs.VideoNode] | None = None
92
) -> vs.VideoNode:
93
    """
94
    Replaces frames in a clip, either with pre-calculated indices or on-the-fly with a callback.
95
    Frame ranges are inclusive. This behaviour can be changed by setting `exclusive=True`.
96

97
    Examples with clips ``black`` and ``white`` of equal length:
98
        * ``replace_ranges(black, white, [(0, 1)])``: replace frames 0 and 1 with ``white``
99
        * ``replace_ranges(black, white, [(None, None)])``: replace the entire clip with ``white``
100
        * ``replace_ranges(black, white, [(0, None)])``: same as previous
101
        * ``replace_ranges(black, white, [(200, None)])``: replace 200 until the end with ``white``
102
        * ``replace_ranges(black, white, [(200, -1)])``: replace 200 until the end with ``white``,
103
                                                         leaving 1 frame of ``black``
104

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

108
    Example of using a callback function:
109
        * ``replace_ranges(clip_a, clip_b, lambda f: get_prop(f, '_PictType', str) == 'P', prop_src=clip_a)``:
110
          Replace frames from ``clip_a`` with ``clip_b`` if the picture type of ``clip_a`` is P.
111

112
    Optional Dependencies:
113
        * `vs-zip <https://github.com/dnjulek/vapoursynth-zip>`_ (highly recommended!)
114

115
    :param clip_a:      Original clip.
116
    :param clip_b:      Replacement clip.
117
    :param ranges:      Ranges to replace clip_a (original clip) with clip_b (replacement clip).
118
                        Integer values in the list indicate single frames,
119
                        Tuple values indicate inclusive ranges.
120
                        Callbacks must return true to replace a with b.
121
                        Negative integer values will be wrapped around based on clip_b's length.
122
                        None values are context dependent:
123
                            * None provided as sole value to ranges: no-op
124
                            * Single None value in list: Last frame in clip_b
125
                            * None as first value of tuple: 0
126
                            * None as second value of tuple: Last frame in clip_b
127
    :param exclusive:   Use exclusive ranges (Default: False).
128
    :param mismatch:    Accept format or resolution mismatch between clips.
129
    :param prop_src:    Source clip(s) to use for frame properties in the callback.
130
                        This is required if you're using a callback.
131

132
    :return:            Clip with ranges from clip_a replaced with clip_b.
133
    """
134

135
    from ..functions import invert_ranges, normalize_ranges
×
136

137
    if ranges != 0 and not ranges or clip_a is clip_b:
×
UNCOV
138
        return clip_a
×
139

UNCOV
140
    if not mismatch:
×
141
        check_ref_clip(clip_a, clip_b)
×
142

UNCOV
143
    if callable(ranges):
×
UNCOV
144
        from inspect import Signature
×
145

146
        signature = Signature.from_callable(ranges, eval_str=True)
×
147

148
        params = set(signature.parameters.keys())
×
149

UNCOV
150
        base_clip = clip_a.std.BlankClip(
×
151
            keep=True, varformat=(clip_a.format != clip_b.format),
152
            varsize=(clip_a.width, clip_a.height) != (clip_b.width, clip_b.height)
153
        )
154

155
        callback: RangesCallback = ranges
×
156

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

UNCOV
164
        if 'f' in params and 'n' in params:
×
165
            def _func(n: int, f: vs.VideoFrame) -> vs.VideoNode:
×
UNCOV
166
                return clip_b if callback(n, f) else clip_a  # type: ignore
×
167
        elif 'f' in params:
×
168
            def _func(n: int, f: vs.VideoFrame) -> vs.VideoNode:
×
UNCOV
169
                return clip_b if callback(f) else clip_a  # type: ignore
×
170
        elif 'n' in params:
×
UNCOV
171
            def _func(n: int) -> vs.VideoNode:  # type: ignore
×
172
                return clip_b if callback(n) else clip_a  # type: ignore
×
173
        else:
UNCOV
174
            raise CustomValueError('Callback must have signature ((n, f) | (n) | (f)) -> bool!')
×
175

176
        _func.__callback = callback  # type: ignore
×
UNCOV
177
        _gc_func_gigacope.append(_func)
×
178

UNCOV
179
        return base_clip.std.FrameEval(_func, prop_src if 'f' in params else None, [clip_a, clip_b])
×
180

UNCOV
181
    shift = 1 - exclusive
×
UNCOV
182
    b_ranges = normalize_ranges(clip_b, ranges)
×
183

UNCOV
184
    if hasattr(vs.core, 'vszip'):
×
UNCOV
185
        return vs.core.vszip.RFS(
×
186
            clip_a, clip_b,
187
            [y for (s, e) in b_ranges
188
             for y in range(
189
                 s, e + (not exclusive if s != e else 1) + (1 if e == clip_b.num_frames - 1 and exclusive else 0)
190
             )
191
            ],
192
            mismatch=mismatch
193
        )
194

UNCOV
195
    a_ranges = invert_ranges(clip_a, clip_b, b_ranges)
×
196

UNCOV
197
    a_trims = [clip_a[max(0, start - exclusive):end + shift + exclusive] for start, end in a_ranges]
×
UNCOV
198
    b_trims = [clip_b[start:end + shift] for start, end in b_ranges]
×
199

200
    if a_ranges:
×
UNCOV
201
        main, other = (a_trims, b_trims) if (a_ranges[0][0] == 0) else (b_trims, a_trims)
×
202
    else:
UNCOV
203
        main, other = (b_trims, a_trims) if (b_ranges[0][0] == 0) else (a_trims, b_trims)
×
204

UNCOV
205
    return vs.core.std.Splice(list(interleave_arr(main, other, 1)), mismatch)
×
206

207

208
def remap_frames(clip: vs.VideoNode, ranges: Sequence[int | tuple[int, int]]) -> vs.VideoNode:
1✔
UNCOV
209
    frame_map = list(flatten(  # type: ignore
×
210
        f if isinstance(f, int) else range(f[0], f[1] + 1) for f in ranges
211
    ))
212

213
    base = clip.std.BlankClip(length=len(frame_map))
×
214

UNCOV
215
    return base.std.FrameEval(lambda n: clip[frame_map[n]], None, clip)
×
216

217

218
def replace_every(
1✔
219
    clipa: vs.VideoNode, clipb: vs.VideoNode, cycle: int, offsets: Sequence[int], modify_duration: bool = True
220
) -> vs.VideoNode:
UNCOV
221
    offsets_a = [x * 2 for x in range(cycle) if x not in offsets]
×
UNCOV
222
    offsets_b = [x * 2 + 1 for x in offsets]
×
UNCOV
223
    offsets = sorted(offsets_a + offsets_b)
×
224

UNCOV
225
    interleaved = vs.core.std.Interleave([clipa, clipb])
×
226

UNCOV
227
    return interleaved.std.SelectEvery(cycle * 2, offsets, modify_duration)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc