preroll 0.0.1__py3-none-any.whl

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.
preroll/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """preroll: tools for editing video podcasts. This early release holds the timecode helpers."""
2
+ from .timecode import (
3
+ frame_rate,
4
+ frames_to_seconds,
5
+ frames_to_timecode,
6
+ seconds_to_frames,
7
+ timecode_to_frames,
8
+ )
9
+
10
+ __version__ = "0.0.1"
11
+ __all__ = [
12
+ "frame_rate",
13
+ "frames_to_seconds",
14
+ "frames_to_timecode",
15
+ "seconds_to_frames",
16
+ "timecode_to_frames",
17
+ ]
preroll/timecode.py ADDED
@@ -0,0 +1,106 @@
1
+ """Frame counts, seconds and SMPTE timecode.
2
+
3
+ A timecode is HH:MM:SS:FF. Drop-frame timecode (29.97 and 59.94 frames a second) is written with a semicolon before
4
+ the frames, HH:MM:SS;FF. It skips frame numbers, never frames: the first 2 numbers (4 at 59.94) of each minute are
5
+ left out, except in each tenth minute, so that the timecode keeps step with the clock.
6
+ """
7
+ from fractions import Fraction
8
+
9
+ _NTSC = {
10
+ Fraction("23.976"): Fraction(24000, 1001),
11
+ Fraction("23.98"): Fraction(24000, 1001),
12
+ Fraction("29.97"): Fraction(30000, 1001),
13
+ Fraction("47.952"): Fraction(48000, 1001),
14
+ Fraction("59.94"): Fraction(60000, 1001),
15
+ Fraction("119.88"): Fraction(120000, 1001),
16
+ }
17
+
18
+
19
+ def frame_rate(fps):
20
+ """The exact frame rate as a Fraction. fps: a number (24, 25, 29.97), a Fraction, or text ("30000/1001",
21
+ "23.976"). The NTSC rates given as decimals are read as their exact values (29.97 is 30000/1001)."""
22
+ if isinstance(fps, bool):
23
+ raise TypeError("frame rate must be a number, a Fraction or text")
24
+ try:
25
+ rate = Fraction(str(fps)) if isinstance(fps, float) else Fraction(fps)
26
+ except (TypeError, ValueError, ZeroDivisionError):
27
+ raise ValueError(f"not a frame rate: {fps!r}") from None
28
+ rate = _NTSC.get(rate, rate)
29
+ if rate <= 0:
30
+ raise ValueError(f"frame rate must be above 0, got {fps!r}")
31
+ return rate
32
+
33
+
34
+ def _base(rate):
35
+ """The whole number of frame numbers in one timecode second (30 for 29.97)."""
36
+ return int(rate + Fraction(1, 2))
37
+
38
+
39
+ def _drop(rate, drop_frame):
40
+ """Frame numbers skipped each minute: 2 at 29.97, 4 at 59.94, 0 for non-drop."""
41
+ can = rate in (Fraction(30000, 1001), Fraction(60000, 1001))
42
+ if drop_frame is None:
43
+ drop_frame = can
44
+ if drop_frame and not can:
45
+ raise ValueError("drop-frame timecode exists only for 29.97 and 59.94 frames a second")
46
+ return 2 * (_base(rate) // 30) if drop_frame else 0
47
+
48
+
49
+ def frames_to_timecode(frames, fps, drop_frame=None):
50
+ """The timecode of a frame count (frame 0 is 00:00:00:00). drop_frame: None uses drop-frame at 29.97 and 59.94
51
+ and non-drop at every other rate."""
52
+ if isinstance(frames, bool) or not isinstance(frames, int):
53
+ raise TypeError("frames must be a whole number")
54
+ if frames < 0:
55
+ raise ValueError("frames must be 0 or more")
56
+ rate = frame_rate(fps)
57
+ base, drop = _base(rate), _drop(rate, drop_frame)
58
+ n = frames
59
+ if drop:
60
+ per_min = base * 60 - drop
61
+ per_10min = base * 600 - 9 * drop
62
+ tens, rest = divmod(frames, per_10min)
63
+ n += 9 * drop * tens
64
+ if rest >= base * 60:
65
+ n += drop * ((rest - base * 60) // per_min + 1)
66
+ ff = n % base
67
+ ss = n // base % 60
68
+ mm = n // (base * 60) % 60
69
+ hh = n // (base * 3600)
70
+ return f"{hh:02d}:{mm:02d}:{ss:02d}{';' if drop else ':'}{ff:02d}"
71
+
72
+
73
+ def timecode_to_frames(timecode, fps, drop_frame=None):
74
+ """The frame count of a timecode. drop_frame: None reads the timecode's own mark (a semicolon or a full stop
75
+ before the frames means drop-frame)."""
76
+ if not isinstance(timecode, str):
77
+ raise TypeError("timecode must be text, for example '00:01:00;02'")
78
+ text = timecode.strip()
79
+ marked = len(text) > 3 and text[-3] in ";."
80
+ parts = text.replace(";", ":").replace(".", ":").split(":")
81
+ if len(parts) != 4 or not all(p.isdigit() for p in parts):
82
+ raise ValueError(f"not a timecode: {timecode!r} (expected HH:MM:SS:FF)")
83
+ hh, mm, ss, ff = (int(p) for p in parts)
84
+ rate = frame_rate(fps)
85
+ base = _base(rate)
86
+ drop = _drop(rate, marked if drop_frame is None else drop_frame)
87
+ if mm > 59 or ss > 59 or ff >= base:
88
+ raise ValueError(f"not a timecode at {base} frames a second: {timecode!r}")
89
+ if drop and ss == 0 and ff < drop and mm % 10:
90
+ raise ValueError(f"drop-frame timecode has no {timecode!r}: frame numbers 0 to {drop - 1} are skipped "
91
+ "at the start of each minute, except each tenth minute")
92
+ minutes = 60 * hh + mm
93
+ return (minutes * 60 + ss) * base + ff - drop * (minutes - minutes // 10)
94
+
95
+
96
+ def frames_to_seconds(frames, fps):
97
+ """The real time of a frame count, in seconds (240 frames at 23.976 are 10.01 s)."""
98
+ return float(frames / frame_rate(fps))
99
+
100
+
101
+ def seconds_to_frames(seconds, fps):
102
+ """The frame that is on screen at a time in seconds (the last frame that starts at or before it)."""
103
+ if seconds < 0:
104
+ raise ValueError("seconds must be 0 or more")
105
+ value = Fraction(str(seconds)) if isinstance(seconds, float) else Fraction(seconds)
106
+ return int(value * frame_rate(fps))
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.5
2
+ Name: preroll
3
+ Version: 0.0.1
4
+ Summary: Tools for editing video podcasts. This early release holds the timecode helpers.
5
+ Author: preroll
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: drop-frame,editing,podcast,smpte,timecode,video
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Multimedia :: Video
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+
16
+ # preroll
17
+
18
+ Tools for editing video podcasts.
19
+
20
+ This is an early release. It holds one part: timecode helpers that convert between frame counts, seconds and SMPTE
21
+ timecode, with drop-frame timecode for 29.97 and 59.94 frames a second. More tools follow.
22
+
23
+ ## Install
24
+
25
+ ```
26
+ pip install preroll
27
+ ```
28
+
29
+ ## Use
30
+
31
+ ```python
32
+ from preroll import frames_to_timecode, timecode_to_frames, frames_to_seconds
33
+
34
+ frames_to_timecode(1800, 29.97) # '00:01:00;02' (drop-frame is the default at 29.97)
35
+ frames_to_timecode(1800, 29.97, drop_frame=False) # '00:01:00:00'
36
+ timecode_to_frames("01:00:00;00", 29.97) # 107892
37
+ timecode_to_frames("00:00:10:00", "24000/1001") # 240
38
+ frames_to_seconds(240, "24000/1001") # 10.01
39
+ ```
40
+
41
+ A frame rate can be a number (`24`, `25`, `29.97`, `23.976`) or an exact fraction as text (`"30000/1001"`). The
42
+ common NTSC rates given as decimals (23.976, 29.97, 59.94) are read as their exact fractions.
43
+
44
+ ## Licence
45
+
46
+ MIT
@@ -0,0 +1,6 @@
1
+ preroll/__init__.py,sha256=PdSbNJ32McJwXupLqjuAhQY74Nn0Gnp8-MPvmByM4lA,389
2
+ preroll/timecode.py,sha256=bTMSdXKkGf4NQJE0FOJTdVeazbD0ZKabeGioMYbJNUw,4631
3
+ preroll-0.0.1.dist-info/METADATA,sha256=jxZ4LN64DrwgThc8vwlWNhDQ2v9sJS6yBvzf-7uqT3Y,1489
4
+ preroll-0.0.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
5
+ preroll-0.0.1.dist-info/licenses/LICENSE,sha256=Uu5Y0z9fboL2smvR3gya2P3zSXUwSwtZ3M5AK4V9Ykk,1064
6
+ preroll-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 preroll
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.