yta-video-frame-time 0.0.6__tar.gz → 0.0.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: yta-video-frame-time
3
- Version: 0.0.6
3
+ Version: 0.0.14
4
4
  Summary: Youtube Autonomous Video Frame Time Module
5
+ License-File: LICENSE
5
6
  Author: danialcala94
6
7
  Author-email: danielalcalavalera@gmail.com
7
8
  Requires-Python: ==3.9
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "yta-video-frame-time"
3
- version = "0.0.6"
3
+ version = "0.0.14"
4
4
  description = "Youtube Autonomous Video Frame Time Module"
5
5
  authors = [
6
6
  {name = "danialcala94",email = "danielalcalavalera@gmail.com"}
@@ -17,6 +17,7 @@ packages = [{include = "yta_video_frame_time", from = "src"}]
17
17
 
18
18
  [tool.poetry.group.dev.dependencies]
19
19
  pytest = "^8.3.5"
20
+ yta_testing = ">=0.0.1"
20
21
 
21
22
  [build-system]
22
23
  requires = ["poetry-core>=2.0.0,<3.0.0"]
@@ -0,0 +1,52 @@
1
+ """
2
+ TODO: I think this module should not be here, or maybe
3
+ yes, but I need this decorator.
4
+ """
5
+ from functools import wraps
6
+
7
+
8
+ def parameter_to_time_interval(
9
+ param_name: str
10
+ ):
11
+ """
12
+ Force the parameter with the given `param_name` to
13
+ be a `TimeInterval` instance.
14
+
15
+ Values accepted:
16
+ - `TimeInterval` instance
17
+ - `tuple[float, float]` that will be `(start, end)`
18
+ """
19
+ def decorator(
20
+ func
21
+ ):
22
+ @wraps(func)
23
+ def wrapper(
24
+ *args,
25
+ **kwargs
26
+ ):
27
+ from inspect import signature
28
+ from yta_validation import PythonValidator
29
+ from yta_video_frame_time.interval import TimeInterval
30
+
31
+ sig = signature(func)
32
+ bound = sig.bind(*args, **kwargs)
33
+ bound.apply_defaults()
34
+
35
+ value = bound.arguments[param_name]
36
+
37
+ if PythonValidator.is_instance_of(value, TimeInterval):
38
+ pass
39
+ elif (
40
+ PythonValidator.is_tuple(value) and
41
+ len(value) == 2
42
+ ):
43
+ value = TimeInterval(*value)
44
+ bound.arguments[param_name] = value
45
+ else:
46
+ raise Exception(f'The "{param_name}" parameter must be a TimeInterval or a tuple[float, float].')
47
+
48
+ return func(*bound.args, **bound.kwargs)
49
+
50
+ return wrapper
51
+
52
+ return decorator
@@ -0,0 +1,951 @@
1
+ """
2
+ TODO: This module is general so we should send it
3
+ to another library related to time intervals and
4
+ not specifically video frame times... Move it.
5
+ """
6
+ from yta_video_frame_time.decorators import parameter_to_time_interval
7
+ from yta_validation.parameter import ParameterValidator
8
+ from quicktions import Fraction
9
+ from typing import Union
10
+
11
+
12
+ Number = Union[int, float, Fraction]
13
+ """
14
+ Custom type to represent numbers.
15
+ """
16
+ TimeIntervalType = Union['TimeInterval', tuple[float, float]]
17
+ """
18
+ The type we accept as time interval, that could be a
19
+ tuple we transform into a time interval.
20
+ """
21
+
22
+ """
23
+ TODO: Maybe we can add the possibility of having an
24
+ `fps` value when initializing it to be able to force
25
+ the time interval values to be multiple of `1/fps`.
26
+ But this, if implemented, should be `TimeIntervalFPS`
27
+ or similar, and inheritance from this one but forcing
28
+ the values to be transformed according to that `1/fps`.
29
+ """
30
+ class TimeInterval:
31
+ """
32
+ Class to represent a time interval, which is a tuple
33
+ of time moments representing the time range
34
+ `[start, end)`.
35
+ """
36
+
37
+ @property
38
+ def duration(
39
+ self
40
+ ) -> float:
41
+ """
42
+ The `duration` of the time interval.
43
+ """
44
+ return self.end - self.start
45
+
46
+ @property
47
+ def copy(
48
+ self
49
+ ) -> 'TimeInterval':
50
+ """
51
+ A copy of this instance.
52
+ """
53
+ return TimeInterval(
54
+ start = self.start,
55
+ end = self.end
56
+ )
57
+
58
+ @property
59
+ def as_tuple(
60
+ self
61
+ ) -> tuple[float, float]:
62
+ """
63
+ The time interval but as a `(start, end)` tuple.
64
+ """
65
+ return (self.start, self.end)
66
+
67
+ @property
68
+ def cutter(
69
+ self
70
+ ) -> 'TimeIntervalCutter':
71
+ """
72
+ Shortcut to the static class `TimeIntervalCutter` that
73
+ is capable of cutting time intervals.
74
+ """
75
+ return TimeIntervalCutter
76
+
77
+ def __init__(
78
+ self,
79
+ start: Number,
80
+ end: Number,
81
+ ):
82
+ """
83
+ Provide the interval as it actually is, with the `start`
84
+ and `end`. These values will be adjusted to an internal
85
+ interval starting on 0.
86
+
87
+ The `end` value must be greater than the `start` value.
88
+ """
89
+ if start > end:
90
+ raise Exception('The `start` value provided is greater than the `end` value provided.')
91
+
92
+ if start == end:
93
+ raise Exception('The `start` value provided is exactly the `end` value provided.')
94
+
95
+ self.start: float = start
96
+ """
97
+ The original `start` of the time segment.
98
+ """
99
+ self.end: float = end
100
+ """
101
+ The original `end` of the time segment.
102
+ """
103
+
104
+ def _validate_t(
105
+ self,
106
+ t: Number,
107
+ do_include_start: bool = False,
108
+ do_include_end: bool = False
109
+ ) -> None:
110
+ """
111
+ Validate that the provided `t` value is between the `start`
112
+ and the `end` parameters provided, including them or not
113
+ according to the boolean parameters provided.
114
+ """
115
+ ParameterValidator.validate_mandatory_number_between(
116
+ name = 't',
117
+ value = t,
118
+ lower_limit = self.start,
119
+ upper_limit = self.end,
120
+ do_include_lower_limit = do_include_start,
121
+ do_include_upper_limit = do_include_end
122
+ )
123
+
124
+ def cut(
125
+ self,
126
+ start: Number,
127
+ end: Number
128
+ ) -> tuple[Union['TimeInterval', None], Union['TimeInterval', None], Union['TimeInterval', None]]:
129
+ """
130
+ Cut a segment from the given `start` to the also provided
131
+ `end` time moments of this time interval instance.
132
+
133
+ This method will return a tuple of 3 elements including the
134
+ segments created by cutting this time interval in the order
135
+ they were generated, but also having the 4th element always
136
+ as the index of the one specifically requested by the user.
137
+ The tuple will include all the segments at the begining and
138
+ the rest will be None (unless the 4th one, which is the
139
+ index).
140
+
141
+ Examples below:
142
+ - A time interval of `[2, 5)` cut with `start=3` and `end=4`
143
+ will generate `((2, 3), (3, 4), (4, 5), 1)`.
144
+ - A time interval of `[2, 5)` cut with `start=2` and `end=4`
145
+ will generate `((2, 4), (4, 5), None, 0)`.
146
+ - A time interval of `[2, 5)` cut with `start=4` and `end=5`
147
+ will generate `((2, 4), (4, 5), None, 1)`.
148
+ - A time interval of `[2, 5)` cut with `start=2` and `end=5`
149
+ will generate `((2, 5), None, None, 0)`.
150
+
151
+ As you can see, the result could be the same in different
152
+ situations, but it's up to you (and the specific method in
153
+ which you are calling to this one) to choose the tuple you
154
+ want to return.
155
+ """
156
+ return self.cutter.from_to(
157
+ time_interval = self,
158
+ start = start,
159
+ end = end
160
+ )
161
+
162
+ def cutted(
163
+ self,
164
+ start: Number,
165
+ end: Number
166
+ ) -> 'TimeInterval':
167
+ """
168
+ Get this time interval instance but cutted from the `start`
169
+ to the `end` time moments provided.
170
+
171
+ (!) This method doesn't modify the original instance but
172
+ returns a new one.
173
+ """
174
+ tuples = self.cut(
175
+ start = start,
176
+ end = end
177
+ )
178
+
179
+ return tuples[tuples[3]]
180
+
181
+ def trim_start(
182
+ self,
183
+ t_variation: Number,
184
+ limit: Union[Number, None] = None
185
+ ) -> tuple['TimeInterval', 'TimeInterval']:
186
+ """
187
+ Get a tuple containing the 2 new `TimeInterval` instances
188
+ generated by trimming this one's start the amount of seconds
189
+ provided as the `t_variation` parameter. The first tuple is
190
+ the remaining, and the second one is the new time interval
191
+ requested by the user.
192
+
193
+ This method will raise an exception if the new `start` value
194
+ becomes a value over the time interval `end` value or the
195
+ `limit`, that must be greater than the `start` and lower
196
+ than the time interval `end` value.
197
+
198
+ The `t_variation` must be a positive value, the amount of
199
+ seconds to be trimmed.
200
+ """
201
+ return self.cutter.trim_start(
202
+ time_interval = self,
203
+ t_variation = t_variation,
204
+ limit = limit
205
+ )
206
+
207
+ def trimmed_start(
208
+ self,
209
+ t_variation: Number,
210
+ limit: Union[Number, None] = None
211
+ ) -> 'TimeInterval':
212
+ """
213
+ Get this time interval instance but trimmed from the `start`
214
+ the `t_variation` amount of seconds provided.
215
+
216
+ (!) This method doesn't modify the original instance but
217
+ returns a new one.
218
+ """
219
+ return self.trim_start(
220
+ t_variation = t_variation,
221
+ limit = limit
222
+ )[1]
223
+
224
+ def trim_end(
225
+ self,
226
+ t_variation: Number,
227
+ limit: Union[Number, None] = None
228
+ ) -> tuple['TimeInterval', 'TimeInterval']:
229
+ """
230
+ Get a tuple containing the 2 new `TimeInterval` instances
231
+ generated by trimming this one's end the amount of seconds
232
+ provided as the `t_variation` parameter. The first tuple is
233
+ the one requested by the user, and the second one is the
234
+ remaining.
235
+
236
+ This method will raise an exception if the new `end` value
237
+ becomes a value under the time interval `start` value or
238
+ the `limit`, that must be greater than the `start` and lower
239
+ than the time interval `end` value.
240
+
241
+ The `t_variation` must be a positive value, the amount of
242
+ seconds to be trimmed.
243
+ """
244
+ return self.cutter.trim_end(
245
+ time_interval = self,
246
+ t_variation = t_variation,
247
+ limit = limit
248
+ )
249
+
250
+ def trimmed_end(
251
+ self,
252
+ t_variation: Number,
253
+ limit: Union[Number, None] = None
254
+ ) -> 'TimeInterval':
255
+ """
256
+ Get this time interval instance but trimmed from the `end`
257
+ the `t_variation` amount of seconds provided.
258
+
259
+ (!) This method doesn't modify the original instance but
260
+ returns a new one.
261
+ """
262
+ return self.trim_end(
263
+ t_variation = t_variation,
264
+ limit = limit
265
+ )[0]
266
+
267
+ def split(
268
+ self,
269
+ t: Number
270
+ ) -> tuple['TimeInterval', 'TimeInterval']:
271
+ """
272
+ Split the time interval at the provided `t` time moment
273
+ and get the 2 new time intervals as a result (as a tuple).
274
+
275
+ This method will raise an exception if the `t` value
276
+ provided is a limit value (or above).
277
+
278
+ Examples below:
279
+ - A time interval of `[2, 5)` cut with `t=3` will generate
280
+ `((2, 3), (3, 5))`.
281
+ - A time interval of `[2, 5)` cut with `t=4` will generate
282
+ `((2, 4), (4, 5))`.
283
+ - A time interval of `[2, 5)` cut with `t>=5` will raise
284
+ exception.
285
+ - A time interval of `[2, 5)` cut with `t<=2` will raise
286
+ exception.
287
+ """
288
+ return self.cutter.split(
289
+ time_interval = self,
290
+ t = t
291
+ )
292
+
293
+ def splitted_left(
294
+ self,
295
+ t: Number
296
+ ) -> 'TimeInterval':
297
+ """
298
+ Split this time interval instance by the `t` time moment
299
+ provided and obtain the time interval from the left side,
300
+ which goes from this time interval `start` time moment to
301
+ the `t` provided.
302
+ """
303
+ return self.split(t)[0]
304
+
305
+ def splitted_right(
306
+ self,
307
+ t: Number
308
+ ) -> 'TimeInterval':
309
+ """
310
+ Split this time interval instance by the `t` time moment
311
+ provided and obtain the time interval from the right side,
312
+ which goes from the `t` provided to the time interval `end`
313
+ time moment.
314
+ """
315
+ return self.split(t)[1]
316
+
317
+ # Other methods below
318
+ def is_t_included(
319
+ self,
320
+ t: float,
321
+ do_include_end: bool = False
322
+ ) -> bool:
323
+ """
324
+ Check if the `t` time moment provided is included in
325
+ this time interval, including the `end` only if the
326
+ `do_include_end` parameter is set as `True`.
327
+ """
328
+ return TimeIntervalUtils.a_includes_t(
329
+ t = t,
330
+ time_interval_a = self,
331
+ do_include_end = do_include_end
332
+ )
333
+
334
+ def is_adjacent_to(
335
+ self,
336
+ time_interval: 'TimeInterval'
337
+ ) -> bool:
338
+ """
339
+ Check if the `time_interval` provided is adjacent
340
+ to this time interval, which means that the `end`
341
+ of one interval is also the `start` of the other
342
+ one.
343
+
344
+ (!) Giving the time intervals inverted will
345
+ provide the same result.
346
+
347
+ Example below:
348
+ - `a=[2, 5)` and `b=[5, 7)` => `True`
349
+ - `a=[5, 7)` and `b=[2, 5)` => `True`
350
+ - `a=[2, 5)` and `b=[3, 4)` => `False`
351
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
352
+ """
353
+ return TimeIntervalUtils.a_is_adjacent_to_b(
354
+ time_interval_a = self,
355
+ time_interval_b = time_interval
356
+ )
357
+
358
+ def do_contains_a(
359
+ self,
360
+ time_interval: 'TimeInterval'
361
+ ) -> bool:
362
+ """
363
+ Check if this time interval includes the `time_interval`
364
+ provided or not, which means that the `time_interval`
365
+ provided is fully contained (included) in this one.
366
+ """
367
+ return TimeIntervalUtils.a_contains_b(
368
+ time_interval_a = self,
369
+ time_interval_b = time_interval
370
+ )
371
+
372
+ def is_contained_in(
373
+ self,
374
+ time_interval: 'TimeInterval'
375
+ ) -> bool:
376
+ """
377
+ Check if this time interval is fully contained in
378
+ the `time_interval` provided, which is a synonim
379
+ of being fully overlapped by that `time_interval`.
380
+ """
381
+ return TimeIntervalUtils.a_is_contained_in_b(
382
+ time_interval_a = self,
383
+ time_interval_b = time_interval
384
+ )
385
+
386
+ def do_intersects_with(
387
+ self,
388
+ time_interval: 'TimeInterval'
389
+ ) -> bool:
390
+ """
391
+ Check if this time interval intersects with the one
392
+ provided as `time_interval`, which means that they
393
+ have at least a part in common.
394
+ """
395
+ return TimeIntervalUtils.a_intersects_with_b(
396
+ time_interval_a = self,
397
+ time_interval_b = time_interval
398
+ )
399
+
400
+ def get_intersection_with_a(
401
+ self,
402
+ time_interval: 'TimeInterval'
403
+ ) -> Union['TimeInterval', None]:
404
+ """
405
+ Get the time interval that intersects this one and the
406
+ one provided as `time_interval`. The result can be `None`
407
+ if there is no intersection in between both.
408
+ """
409
+ return TimeIntervalUtils.get_intersection_of_a_and_b(
410
+ time_interval_a = self,
411
+ time_interval_b = time_interval
412
+ )
413
+
414
+
415
+ class TimeIntervalUtils:
416
+ """
417
+ Static class to wrap the utils related to time intervals.
418
+ """
419
+
420
+ @staticmethod
421
+ def a_includes_t(
422
+ t: float,
423
+ time_interval_a: 'TimeInterval',
424
+ do_include_end: bool = False
425
+ ) -> bool:
426
+ """
427
+ Check if the `t` time moment provided is included in
428
+ the `time_interval_a` given. The `time_interval_a.end`
429
+ is excluded unless the `do_include_end` parameter is
430
+ set as `True`.
431
+
432
+ A time interval is `[start, end)`, thats why the end is
433
+ excluded by default.
434
+ """
435
+ return (
436
+ time_interval_a.start <= t <= time_interval_a.end
437
+ if do_include_end else
438
+ time_interval_a.start <= t < time_interval_a.end
439
+ )
440
+
441
+ @staticmethod
442
+ def a_is_adjacent_to_b(
443
+ time_interval_a: 'TimeInterval',
444
+ time_interval_b: 'TimeInterval',
445
+ ) -> bool:
446
+ """
447
+ Check if the `time_interval_a` provided and the
448
+ also given `time_interval_b` are adjacent, which
449
+ means that the `end` of one interval is also the
450
+ `start` of the other one.
451
+
452
+ (!) Giving the time intervals inverted will
453
+ provide the same result.
454
+
455
+ Examples below:
456
+ - `a=[2, 5)` and `b=[5, 7)` => `True`
457
+ - `a=[5, 7)` and `b=[2, 5)` => `True`
458
+ - `a=[2, 5)` and `b=[3, 4)` => `False`
459
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
460
+ """
461
+ return (
462
+ TimeIntervalUtils.a_is_inmediately_before_b(time_interval_a, time_interval_b) or
463
+ TimeIntervalUtils.a_is_inmediately_after_b(time_interval_a, time_interval_b)
464
+ )
465
+
466
+ @staticmethod
467
+ def a_is_inmediately_before_b(
468
+ time_interval_a: 'TimeInterval',
469
+ time_interval_b: 'TimeInterval',
470
+ ) -> bool:
471
+ """
472
+ Check if the `time_interval_a` provided is inmediately
473
+ before the also given `time_interval_b`, which means
474
+ that the `end` of the first one is also the `start` of
475
+ the second one.
476
+
477
+ Examples below:
478
+ - `a=[2, 5)` and `b=[5, 7)` => `True`
479
+ - `a=[5, 7)` and `b=[2, 5)` => `False`
480
+ - `a=[2, 5)` and `b=[3, 4)` => `False`
481
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
482
+ """
483
+ return time_interval_a.end == time_interval_b.start
484
+
485
+ @staticmethod
486
+ def a_is_inmediately_after_b(
487
+ time_interval_a: 'TimeInterval',
488
+ time_interval_b: 'TimeInterval',
489
+ ) -> bool:
490
+ """
491
+ Check if the `time_interval_a` provided is inmediately
492
+ after the also given `time_interval_b`, which means
493
+ that the `start` of the first one is also the `end` of
494
+ the second one.
495
+
496
+ Examples below:
497
+ - `a=[2, 5)` and `b=[5, 7)` => `False`
498
+ - `a=[5, 7)` and `b=[2, 5)` => `True`
499
+ - `a=[2, 5)` and `b=[3, 4)` => `False`
500
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
501
+ """
502
+ return time_interval_a.start == time_interval_b.end
503
+
504
+ @staticmethod
505
+ def a_contains_b(
506
+ time_interval_a: 'TimeInterval',
507
+ time_interval_b: 'TimeInterval'
508
+ ) -> bool:
509
+ """
510
+ Check if the `time_interval_a` time interval provided
511
+ includes the `time_interval_b` or not, which means that
512
+ the `time_interval_b` is fully contained in the first
513
+ one.
514
+
515
+ Examples below:
516
+ - `a=[2, 5)` and `b=[3, 4)` => `True`
517
+ - `a=[2, 5)` and `b=[2, 4)` => `True`
518
+ - `a=[2, 5)` and `b=[3, 6)` => `False`
519
+ - `a=[2, 5)` and `b=[6, 8)` => `False`
520
+ """
521
+ return (
522
+ time_interval_a.start <= time_interval_b.start and
523
+ time_interval_a.end >= time_interval_b.end
524
+ )
525
+
526
+ @staticmethod
527
+ def a_is_contained_in_b(
528
+ time_interval_a: 'TimeInterval',
529
+ time_interval_b: 'TimeInterval',
530
+ ) -> bool:
531
+ """
532
+ Check if the `time_interval_a` provided is fully
533
+ contained into the also provided `time_interval_b`.
534
+
535
+ Examples below:
536
+ - `a=[2, 5)` and `b=[1, 6)` => `True`
537
+ - `a=[2, 5)` and `b=[0, 9)` => `True`
538
+ - `a=[2, 5)` and `b=[2, 4)` => `False`
539
+ - `a=[2, 5)` and `b=[4, 8)` => `False`
540
+ - `a=[2, 5)` and `b=[7, 8)` => `False`
541
+ """
542
+ return TimeIntervalUtils.a_contains_b(
543
+ time_interval_a = time_interval_b,
544
+ time_interval_b = time_interval_a
545
+ )
546
+
547
+ @staticmethod
548
+ def a_intersects_with_b(
549
+ time_interval_a: 'TimeInterval',
550
+ time_interval_b: 'TimeInterval',
551
+ ) -> bool:
552
+ """
553
+ Check if the `time_interval_a` and the `time_interval_b`
554
+ provided has at least a part in common.
555
+
556
+ Examples below:
557
+ - `a=[2, 5)` and `b=[4, 6)` => `True`
558
+ - `a=[2, 5)` and `b=[1, 3)` => `True`
559
+ - `a=[2, 5)` and `b=[5, 6)` => `False`
560
+ - `a=[2, 5)` and `b=[7, 8)` => `False`
561
+ - `a=[2, 5)` and `b=[1, 2)` => `False`
562
+ """
563
+ return (
564
+ time_interval_b.start < time_interval_a.end and
565
+ time_interval_a.start < time_interval_b.end
566
+ )
567
+
568
+ @staticmethod
569
+ def get_intersection_of_a_and_b(
570
+ time_interval_a: 'TimeInterval',
571
+ time_interval_b: 'TimeInterval'
572
+ ) -> Union['TimeInterval', None]:
573
+ """
574
+ Get the time interval that intersects the two time
575
+ intervals provided, that can be `None` if there is no
576
+ intersection in between both.
577
+ """
578
+ return (
579
+ None
580
+ if not TimeIntervalUtils.a_intersects_with_b(
581
+ time_interval_a = time_interval_a,
582
+ time_interval_b = time_interval_b
583
+ ) else
584
+ TimeInterval(
585
+ start = max(time_interval_a.start, time_interval_b.start),
586
+ end = min(time_interval_a.end, time_interval_b.end)
587
+ )
588
+ )
589
+
590
+ class TimeIntervalCutter:
591
+ """
592
+ Class to wrap the functionality related to cutting
593
+ time intervals.
594
+ """
595
+
596
+ """
597
+ TODO: Methods I have to implement:
598
+ trim_end → recorta la parte final
599
+ trim_start → recorta la parte inicial
600
+ cut_segment → elimina un tramo [a, b)
601
+ split_at → divide en dos en un punto dado
602
+ """
603
+
604
+ @staticmethod
605
+ @parameter_to_time_interval('time_interval')
606
+ def trim_end_to(
607
+ time_interval: TimeIntervalType,
608
+ t: Number
609
+ ) -> tuple['TimeInterval', 'TimeInterval']:
610
+ """
611
+ Get a tuple containing the 2 new `TimeInterval` instances
612
+ generated by trimming the `time_interval` end to the `t`
613
+ time moment provided. The first tuple is the requested by
614
+ the user, and the second one is the remaining.
615
+
616
+ The `t` time moment provided must be a value between the
617
+ `start` and `end` of the `time_interval` provided.
618
+ """
619
+ time_interval._validate_t(t)
620
+
621
+ return (
622
+ TimeInterval(
623
+ start = time_interval.start,
624
+ end = t
625
+ ),
626
+ TimeInterval(
627
+ start = t,
628
+ end = time_interval.end
629
+ )
630
+ )
631
+
632
+ @staticmethod
633
+ @parameter_to_time_interval('time_interval')
634
+ def trim_end(
635
+ time_interval: TimeIntervalType,
636
+ t_variation: Number,
637
+ limit: Union[Number, None] = None,
638
+ ) -> tuple['TimeInterval', 'TimeInterval']:
639
+ """
640
+ Get a tuple containing the 2 new `TimeInterval` instances
641
+ generated by trimming the `time_interval` end the amount
642
+ of seconds provided as the `t_variation` parameter. The
643
+ first tuple is the requested by the user, and the second one
644
+ is the remaining.
645
+
646
+ This method will raise an exception if the new `end` value
647
+ becomes a value under the time interval `start` value or
648
+ the `limit`, that must be greater than the `start` and lower
649
+ than the time interval `end` value.
650
+
651
+ The `t_variation` must be a positive value, the amount of
652
+ seconds to be trimmed.
653
+ """
654
+ ParameterValidator.validate_mandatory_positive_number('t_variation', t_variation, do_include_zero = False)
655
+ ParameterValidator.validate_number_between('limit', limit, time_interval.start, time_interval.end, False, False)
656
+
657
+ new_end = time_interval.end - t_variation
658
+ limit = (
659
+ time_interval.start
660
+ if limit is None else
661
+ limit
662
+ )
663
+
664
+ if new_end <= time_interval.start:
665
+ raise Exception('The "t_variation" value provided makes the new end value be lower than the "start" value.')
666
+
667
+ if new_end <= limit:
668
+ raise Exception('The "t_variation" value provided makes the new end value be lower than the "limit" value provided.')
669
+
670
+ return (
671
+ TimeInterval(
672
+ start = time_interval.start,
673
+ end = new_end
674
+ ),
675
+ TimeInterval(
676
+ start = new_end,
677
+ end = time_interval.end
678
+ )
679
+ )
680
+
681
+ @staticmethod
682
+ @parameter_to_time_interval('time_interval')
683
+ def trim_start_to(
684
+ time_interval: TimeIntervalType,
685
+ t: Number
686
+ ) -> tuple['TimeInterval', 'TimeInterval']:
687
+ """
688
+ Get a tuple containing the 2 new `TimeInterval` instances
689
+ generated by trimming the `time_interval` start to the `t`
690
+ time moment provided. The first tuple is the remaining, and
691
+ the second one is the requested by the user.
692
+
693
+ The `t` time moment provided must be a value between the
694
+ `start` and `end` of the `time_interval` provided.
695
+ """
696
+ time_interval._validate_t(t)
697
+
698
+ return (
699
+ TimeInterval(
700
+ start = time_interval.start,
701
+ end = t
702
+ ),
703
+ TimeInterval(
704
+ start = t,
705
+ end = time_interval.end
706
+ )
707
+ )
708
+
709
+ @staticmethod
710
+ @parameter_to_time_interval('time_interval')
711
+ def trim_start(
712
+ time_interval: TimeIntervalType,
713
+ t_variation: Number,
714
+ limit: Union[Number, None] = None,
715
+ ) -> tuple['TimeInterval', 'TimeInterval']:
716
+ """
717
+ Get a tuple containing the 2 new `TimeInterval` instances
718
+ generated by trimming the `time_interval` start the amount
719
+ of seconds provided as the `t_variation` parameter. The
720
+ first tuple is the remaining, and the second one is the
721
+ new time interval requested by the user.
722
+
723
+ This method will raise an exception if the new `end` value
724
+ becomes a value under the time interval `start` value or
725
+ the `limit`, that must be greater than the `start` and lower
726
+ than the time interval `end` value.
727
+
728
+ The `t_variation` must be a positive value, the amount of
729
+ seconds to be trimmed.
730
+ """
731
+ ParameterValidator.validate_mandatory_positive_number('t_variation', t_variation, do_include_zero = False)
732
+ ParameterValidator.validate_number_between('limit', limit, time_interval.start, time_interval.end, False, False)
733
+
734
+ new_start = time_interval.start + t_variation
735
+ limit = (
736
+ time_interval.end
737
+ if limit is None else
738
+ limit
739
+ )
740
+
741
+ if new_start >= time_interval.end:
742
+ raise Exception('The "t_variation" value provided makes the new end value be lower than the "end" value.')
743
+
744
+ if new_start >= limit:
745
+ raise Exception('The "t_variation" value provided makes the new end value be greater than the "limit" value provided.')
746
+
747
+ return (
748
+ TimeInterval(
749
+ start = time_interval.start,
750
+ end = new_start
751
+ ),
752
+ TimeInterval(
753
+ start = new_start,
754
+ end = time_interval.end
755
+ )
756
+ )
757
+
758
+ @staticmethod
759
+ @parameter_to_time_interval('time_interval')
760
+ def from_to(
761
+ time_interval: 'TimeInterval',
762
+ start: Number,
763
+ end: Number
764
+ ) -> tuple[Union['TimeInterval', None], Union['TimeInterval', None], Union['TimeInterval', None], int]:
765
+ """
766
+ Cut a segment from the given `start` to the also provided
767
+ `end` time moments of the `time_interval` passed as
768
+ parameter.
769
+
770
+ This method will return a tuple of 3 elements including the
771
+ segments created by cutting this time interval in the order
772
+ they were generated, but also having the 4th element always
773
+ as the index of the one specifically requested by the user.
774
+ The tuple will include all the segments at the begining and
775
+ the rest will be None (unless the 4th one, which is the
776
+ index).
777
+
778
+ Examples below:
779
+ - A time interval of `[2, 5)` cut with `start=3` and `end=4`
780
+ will generate `((2, 3), (3, 4), (4, 5), 1)`.
781
+ - A time interval of `[2, 5)` cut with `start=2` and `end=4`
782
+ will generate `((2, 4), (4, 5), None, 0)`.
783
+ - A time interval of `[2, 5)` cut with `start=4` and `end=5`
784
+ will generate `((2, 4), (4, 5), None, 1)`.
785
+ - A time interval of `[2, 5)` cut with `start=2` and `end=5`
786
+ will generate `((2, 5), None, None, 0)`.
787
+
788
+ As you can see, the result could be the same in different
789
+ situations, but it's up to you (and the specific method in
790
+ which you are calling to this one) to choose the tuple you
791
+ want to return.
792
+ """
793
+ time_interval._validate_t(start, do_include_start = True)
794
+ time_interval._validate_t(end, do_include_end = True)
795
+
796
+ return (
797
+ # TODO: What about this case, should we raise except (?)
798
+ (
799
+ time_interval.copy,
800
+ None,
801
+ None,
802
+ 0
803
+ )
804
+ if (
805
+ start == time_interval.start and
806
+ end == time_interval.end
807
+ ) else
808
+ (
809
+ TimeInterval(
810
+ start = time_interval.start,
811
+ end = end
812
+ ),
813
+ TimeInterval(
814
+ start = end,
815
+ end = time_interval.end
816
+ ),
817
+ None,
818
+ 0
819
+ )
820
+ if start == time_interval.start else
821
+ (
822
+ TimeInterval(
823
+ start = time_interval.start,
824
+ end = start
825
+ ),
826
+ TimeInterval(
827
+ start = start,
828
+ end = time_interval.end
829
+ ),
830
+ None,
831
+ 1
832
+ )
833
+ if end == time_interval.end else
834
+ (
835
+ TimeInterval(
836
+ start = time_interval.start,
837
+ end = start
838
+ ),
839
+ TimeInterval(
840
+ start = start,
841
+ end = end
842
+ ),
843
+ TimeInterval(
844
+ start = end,
845
+ end = time_interval.end
846
+ ),
847
+ 1
848
+ )
849
+ )
850
+
851
+ @staticmethod
852
+ @parameter_to_time_interval('time_interval')
853
+ def split(
854
+ time_interval: 'TimeInterval',
855
+ t: Number,
856
+ ) -> tuple['TimeInterval', 'TimeInterval']:
857
+ """
858
+ Split the interval at the provided `t` time moment and
859
+ get the 2 new time intervals as a result (as a tuple).
860
+
861
+ This method will raise an exception if the `t` value
862
+ provided is a limit value (or above).
863
+
864
+ Examples below:
865
+ - A time interval of `[2, 5)` cut with `t=3` will generate
866
+ `((2, 3), (3, 5))`.
867
+ - A time interval of `[2, 5)` cut with `t=4` will generate
868
+ `((2, 4), (4, 5))`.
869
+ - A time interval of `[2, 5)` cut with `t>=5` will raise
870
+ exception.
871
+ - A time interval of `[2, 5)` cut with `t<=2` will raise
872
+ exception.
873
+ """
874
+ if (
875
+ t <= time_interval.start or
876
+ t >= time_interval.end
877
+ ):
878
+ raise Exception('The "t" value provided is not a valid value as it is a limit (or more than a limit).')
879
+
880
+ return (
881
+ TimeInterval(
882
+ start = time_interval.start,
883
+ end = t
884
+ ),
885
+ TimeInterval(
886
+ start = t,
887
+ end = time_interval.end
888
+ )
889
+ )
890
+
891
+ # # TODO: This method is interesting if we don't want only to
892
+ # # trim but also to extend, so we can use the limits.
893
+ # @staticmethod
894
+ # def trim_end(
895
+ # time_interval: TimeIntervalType,
896
+ # t_variation: Number,
897
+ # lower_limit: Number = 0.0,
898
+ # upper_limit: Union[Number, None] = None,
899
+ # do_include_lower_limit: bool = True,
900
+ # do_include_upper_limit: bool = True
901
+ # ) -> Union['TimeInterval', 'TimeInterval']:
902
+ # """
903
+ # Get a tuple containing the 2 new `TimeInterval` instances
904
+ # generated by trimming the `time_interval` provided from
905
+ # the `end` and at the `t` time moment provided. The first
906
+ # value is the remaining, and the second one is the requested
907
+ # by the user.
908
+
909
+ # This will raise an exception if the new `end` becomes a
910
+ # value under the `lower_limit` provided, above the
911
+ # `upper_limit` given (if given) or even below the time
912
+ # interval `start` value. The limits will be included or not
913
+ # according to the boolean parameters.
914
+ # """
915
+ # new_end = time_interval.end + t_variation
916
+
917
+ # if new_end <= time_interval.start:
918
+ # raise Exception('The "t_variation" value provided makes the new end value be lower than the "start" value.')
919
+
920
+ # is_under_lower_limit = (
921
+ # new_end < lower_limit
922
+ # if do_include_lower_limit else
923
+ # new_end <= lower_limit
924
+ # )
925
+
926
+ # is_over_upper_limit = (
927
+ # upper_limit is None and
928
+ # (
929
+ # new_end > upper_limit
930
+ # if do_include_upper_limit else
931
+ # new_end >= upper_limit
932
+ # )
933
+ # )
934
+
935
+ # if (
936
+ # is_under_lower_limit or
937
+ # is_over_upper_limit
938
+ # ):
939
+ # raise Exception('The "t_variation" value provided makes the new end value be out of the limits.')
940
+
941
+ # return (
942
+ # TimeInterval(
943
+ # start = time_interval.start,
944
+ # end = new_end
945
+ # ),
946
+ # TimeInterval(
947
+ # start = new_end,
948
+ # end = time_interval.end
949
+ # )
950
+ # )
951
+
@@ -238,6 +238,8 @@ def round_pts(
238
238
  and 'time_base', but here is an easier
239
239
  example using the time moments.
240
240
 
241
+ (!) This is valid only for video.
242
+
241
243
  Examples below, with `time_base = 1/5`:
242
244
  - `t = 0.25` => `0.2` (truncated or rounded)
243
245
  - `t = 0.35` => `0.2` (truncated)
@@ -256,8 +258,6 @@ def round_pts(
256
258
 
257
259
  return int(frame_index * ticks_per_frame)
258
260
 
259
- # TODO: Create a 'round_pts'
260
-
261
261
  """
262
262
  When we are working with the 't' time
263
263
  moment we need to use the fps, and when
@@ -302,7 +302,22 @@ class _T:
302
302
  None, we will not make any conversion
303
303
  and the value received could be useless
304
304
  because it is in the middle of a range.
305
+
306
+ The formula:
307
+ - `pts * time_base`
305
308
  """
309
+ t = Fraction(pts * self._t_handler.time_base)
310
+
311
+ return (
312
+ self._t_handler.t.truncated(t)
313
+ if do_truncate is True else
314
+ self._t_handler.t.rounded(t)
315
+ if do_truncate is False else
316
+ t # if None
317
+ )
318
+
319
+ # TODO: Remove this below in the next
320
+ # commit
306
321
  pts = (
307
322
  self._t_handler.pts.truncated(pts)
308
323
  if do_truncate is True else
@@ -322,13 +337,52 @@ class _T:
322
337
  Transform the given 't' to a 'pts' value
323
338
  truncating, rounding or applying no
324
339
  variation.
340
+
341
+ The formula:
342
+ - `int(t / time_base)`
325
343
  """
326
344
  return self._t_handler.pts.from_t(t, do_truncate)
345
+
346
+ def to_index(
347
+ self,
348
+ t: Union[int, float, Fraction],
349
+ do_truncate: Union[bool, None] = True
350
+ ) -> int:
351
+ """
352
+ Transform the given 't' to a index value
353
+ truncating, rounding or applying no
354
+ variation.
355
+
356
+ The formula:
357
+ - `int(round(t * fps))`
358
+ """
359
+ t = (
360
+ self.truncated(t)
361
+ if do_truncate is True else
362
+ self.rounded(t)
363
+ if do_truncate is False else
364
+ t
365
+ )
366
+
367
+ return frame_t_to_index(t, self.fps)
368
+
369
+ def from_index(
370
+ self,
371
+ index: int
372
+ ) -> Fraction:
373
+ """
374
+ Transform the given index to a 't' time
375
+ moment value.
376
+
377
+ The formula:
378
+ - `frame_index * (1 / fps)`
379
+ """
380
+ return frame_index_to_t(index, self.fps)
327
381
 
328
382
  def truncated(
329
383
  self,
330
384
  t: Union[int, float, Fraction]
331
- ):
385
+ ) -> Fraction:
332
386
  """
333
387
  Get the 't' value provided but truncated.
334
388
 
@@ -338,10 +392,26 @@ class _T:
338
392
  """
339
393
  return round_t(t, Fraction(1, self._t_handler.fps), do_truncate = True)
340
394
 
395
+ def is_truncated(
396
+ self,
397
+ t: Union[int, float, Fraction]
398
+ ) -> bool:
399
+ """
400
+ Check if the `t` value provided is the truncated
401
+ value, which means that the `t` provided is the
402
+ `start` from the `[start, end)` range defined by
403
+ the fps.
404
+ """
405
+ return check_values_are_same(
406
+ value_a = t,
407
+ value_b = self.truncated(t),
408
+ tolerance = 0.000001
409
+ )
410
+
341
411
  def rounded(
342
412
  self,
343
413
  t: Union[int, float, Fraction]
344
- ):
414
+ ) -> Fraction:
345
415
  """
346
416
  Get the 't' value provided but rounded.
347
417
 
@@ -367,6 +437,9 @@ class _T:
367
437
 
368
438
  Useful when you need the next value for a
369
439
  range in an iteration or similar.
440
+
441
+ The formula:
442
+ - `t + n * (1 / fps)`
370
443
  """
371
444
  t = (
372
445
  self.truncated(t)
@@ -374,7 +447,7 @@ class _T:
374
447
  self.rounded(t)
375
448
  )
376
449
 
377
- return t + n * self._t_handler.time_base
450
+ return t + n * (1 / self._t_handler.fps)
378
451
 
379
452
  def previous(
380
453
  self,
@@ -395,6 +468,9 @@ class _T:
395
468
  Be careful, if the 'truncated' value is 0
396
469
  this will give you an unexpected negative
397
470
  value.
471
+
472
+ The formula:
473
+ - `t - n * (1 / fps)`
398
474
  """
399
475
  t = (
400
476
  self.truncated(t)
@@ -402,7 +478,7 @@ class _T:
402
478
  self.rounded(t)
403
479
  )
404
480
 
405
- return t - n * self._t_handler.time_base
481
+ return t - n * (1 / self._t_handler.fps)
406
482
 
407
483
  class _Pts:
408
484
  """
@@ -442,6 +518,9 @@ class _Pts:
442
518
  None, we will not make any conversion
443
519
  and the value received could be useless
444
520
  because it is in the middle of a range.
521
+
522
+ The formula:
523
+ - `int(t / time_base)`
445
524
  """
446
525
  t = (
447
526
  self._t_handler.t.truncated(t)
@@ -462,14 +541,70 @@ class _Pts:
462
541
  Transform the given 'pts' to a 't' value
463
542
  truncating, rounding or applying no
464
543
  variation.
544
+
545
+ The formula:
546
+ - `pts * time_base`
465
547
  """
466
548
  return self._t_handler.t.from_pts(pts, do_truncate)
549
+
550
+ def to_index(
551
+ self,
552
+ pts: int,
553
+ do_truncate: Union[bool, None] = True
554
+ ) -> int:
555
+ """
556
+ Transform the given 'pts' to a index value
557
+ truncating, rounding or applying no
558
+ variation.
559
+
560
+ The formula:
561
+ - `int(round((pts * time_base) * fps))`
562
+ """
563
+ return self._t_handler.t.to_index(
564
+ self.to_t(pts, do_truncate = None),
565
+ do_truncate = do_truncate
566
+ )
567
+
568
+ def from_index(
569
+ self,
570
+ index: int
571
+ ) -> Fraction:
572
+ """
573
+ Transform the given index to a 't' time
574
+ moment value.
575
+
576
+ The formula:
577
+ - `int((frame_index * (1 / fps)) * time_base)`
578
+ """
579
+ return self.from_t(
580
+ t = self._t_handler.t.from_index(index),
581
+ do_truncate = True
582
+ )
583
+
584
+ """
585
+ These 2 methods below are here because they
586
+ seem to work for videos, but I think they
587
+ could work not if the video has dynamic frame
588
+ rate or in some other situations, thats why
589
+ this is here as a reminder.
590
+
591
+ I found one video that had audio_fps=44100
592
+ and time_base=256/11025, so it was impossible
593
+ to make a conversion using this formula with
594
+ the audio. With video seems to be ok, but...
595
+
596
+ Use these methods below at your own risk.
597
+ """
467
598
 
468
599
  def truncated(
469
600
  self,
470
601
  pts: int
471
602
  ):
472
603
  """
604
+ (!) This is valid only for video and/or
605
+ could work not properly. Use it at your
606
+ own risk.
607
+
473
608
  Get the 'pts' value provided but truncated.
474
609
 
475
610
  This means that if 't' is in a
@@ -488,6 +623,10 @@ class _Pts:
488
623
  pts: int
489
624
  ) -> int:
490
625
  """
626
+ (!) This is valid only for video and/or
627
+ could work not properly. Use it at your
628
+ own risk.
629
+
491
630
  Get the 'pts' value provided but rounded.
492
631
 
493
632
  This means that if 't' is in a
@@ -510,6 +649,10 @@ class _Pts:
510
649
  do_truncate: bool = True
511
650
  ) -> int:
512
651
  """
652
+ (!) This is valid only for video and/or
653
+ could work not properly. Use it at your
654
+ own risk.
655
+
513
656
  Get the value that is 'n' times ahead of
514
657
  the 'pts' value provided (truncated or
515
658
  rounded according to the 'do_truncate'
@@ -517,6 +660,9 @@ class _Pts:
517
660
 
518
661
  Useful when you need the next value for a
519
662
  range in an iteration or similar.
663
+
664
+ The formula:
665
+ - `pts + n * ticks_per_frame`
520
666
  """
521
667
  pts = (
522
668
  self.truncated(pts)
@@ -533,6 +679,10 @@ class _Pts:
533
679
  do_truncate: bool = True
534
680
  ) -> int:
535
681
  """
682
+ (!) This is valid only for video and/or
683
+ could work not properly. Use it at your
684
+ own risk.
685
+
536
686
  Get the value that is 'n' times before
537
687
  the 't' property of this instance
538
688
  (truncated or rounded according to the
@@ -545,6 +695,9 @@ class _Pts:
545
695
  Be careful, if the 'truncated' value is 0
546
696
  this will give you an unexpected negative
547
697
  value.
698
+
699
+ The formula:
700
+ - `pts - n * ticks_per_frame`
548
701
  """
549
702
  pts = (
550
703
  self.truncated(pts)
@@ -553,7 +706,7 @@ class _Pts:
553
706
  )
554
707
 
555
708
  return pts - n * get_ticks_per_frame(self._t_handler.fps, self._t_handler.time_base)
556
-
709
+
557
710
  class THandler:
558
711
  """
559
712
  Class to simplify the way we work with
@@ -621,9 +774,9 @@ def frame_t_to_index(
621
774
  also provided 'fps'.
622
775
 
623
776
  The formula:
624
- - `int(t * fps)`
777
+ - `int(round(t * fps))`
625
778
  """
626
- return int(parse_fraction(t) * fps)
779
+ return int(round(t * fps))
627
780
 
628
781
  def frame_index_to_t(
629
782
  index: int,
@@ -651,6 +804,8 @@ def frame_t_to_pts(
651
804
  moment provided, based on the also provided
652
805
  'fps' and 'time_base'.
653
806
 
807
+ (!) This is valid only for videos.
808
+
654
809
  The formula:
655
810
  - `frame_index * ticks_per_frame`
656
811
  """
@@ -670,6 +825,23 @@ def frame_pts_to_t(
670
825
  - `pts * time_base`
671
826
  """
672
827
  return parse_fraction(pts * time_base)
828
+
829
+ def get_audio_frame_duration(
830
+ samples: int,
831
+ audio_fps: Fraction
832
+ ) -> Fraction:
833
+ """
834
+ Get the audio frame duration by giving the
835
+ number of '.samples' and also the rate (that
836
+ we call 'audio_fps').
837
+
838
+ This is useful when trying to guess the next
839
+ pts or t.
840
+
841
+ The formula:
842
+ - `samples / audio_fps`
843
+ """
844
+ return Fraction(samples / audio_fps)
673
845
 
674
846
  def get_ticks_per_frame(
675
847
  fps: Union[float, int, Fraction],
@@ -680,6 +852,9 @@ def get_ticks_per_frame(
680
852
  tick is the minimum amount of time we
681
853
  spend from one frame to the next.
682
854
 
855
+ (!) This is only valid for video
856
+ apparently.
857
+
683
858
  The formula:
684
859
  - `1 / (fps * time_base)`
685
860
  """
@@ -723,3 +898,17 @@ def parse_fraction(
723
898
 
724
899
  return fraction
725
900
 
901
+ def check_values_are_same(
902
+ value_a: Union[Fraction, float],
903
+ value_b: Union[Fraction, float],
904
+ tolerance: float = 1e-6
905
+ ) -> bool:
906
+ """
907
+ Check that the `value_a` and the `value_b` are the same
908
+ by applying the `tolerance` value, that is 0.000001 by
909
+ default.
910
+
911
+ For example, `0.016666666666666666` is the same value as
912
+ `1/60` with `tolerance=0.000001`.
913
+ """
914
+ return abs(float(value_a) - float(value_b)) < tolerance