pycmx 1.3.0__tar.gz → 1.5.0__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.
- {pycmx-1.3.0 → pycmx-1.5.0}/PKG-INFO +28 -19
- {pycmx-1.3.0 → pycmx-1.5.0}/README.md +12 -5
- pycmx-1.5.0/pyproject.toml +80 -0
- {pycmx-1.3.0 → pycmx-1.5.0/src}/pycmx/__init__.py +2 -0
- pycmx-1.5.0/src/pycmx/cdl.py +48 -0
- {pycmx-1.3.0 → pycmx-1.5.0/src}/pycmx/channel_map.py +1 -1
- pycmx-1.5.0/src/pycmx/edit.py +213 -0
- {pycmx-1.3.0 → pycmx-1.5.0/src}/pycmx/edit_list.py +17 -16
- {pycmx-1.3.0 → pycmx-1.5.0/src}/pycmx/event.py +49 -11
- {pycmx-1.3.0 → pycmx-1.5.0/src}/pycmx/parse_cmx_events.py +6 -6
- pycmx-1.5.0/src/pycmx/parse_cmx_statements.py +266 -0
- pycmx-1.5.0/src/pycmx/statements.py +102 -0
- {pycmx-1.3.0 → pycmx-1.5.0/src}/pycmx/transition.py +3 -2
- {pycmx-1.3.0 → pycmx-1.5.0/src}/pycmx/util.py +1 -1
- pycmx-1.3.0/pycmx/edit.py +0 -133
- pycmx-1.3.0/pycmx/parse_cmx_statements.py +0 -193
- pycmx-1.3.0/pyproject.toml +0 -56
- {pycmx-1.3.0 → pycmx-1.5.0}/LICENSE +0 -0
|
@@ -1,31 +1,34 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pycmx
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Python CMX 3600 Edit Decision List Parser
|
|
5
|
-
Home-page: https://github.com/iluvcapra/pycmx
|
|
6
|
-
License: MIT
|
|
7
5
|
Keywords: parser,film,broadcast
|
|
8
6
|
Author: Jamie Hardt
|
|
9
|
-
Author-email: jamiehardt@me.com
|
|
10
|
-
|
|
7
|
+
Author-email: Jamie Hardt <jamiehardt@me.com>
|
|
8
|
+
License-File: LICENSE
|
|
11
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
-
Classifier:
|
|
11
|
+
Classifier: Topic :: Multimedia
|
|
12
|
+
Classifier: Topic :: Multimedia :: Video
|
|
13
|
+
Classifier: Topic :: Text Processing
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.8
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
-
Classifier:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Requires-Dist: sphinx
|
|
25
|
-
Requires-
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Requires-Dist: pytest ; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.14.10 ; extra == 'dev'
|
|
23
|
+
Requires-Dist: sphinx>=5.3.0 ; extra == 'doc'
|
|
24
|
+
Requires-Dist: sphinx-rtd-theme>=1.1.1 ; extra == 'doc'
|
|
25
|
+
Requires-Python: >3.8
|
|
26
|
+
Project-URL: Homepage, https://github.com/iluvcapra/pycmx
|
|
26
27
|
Project-URL: Documentation, https://pycmx.readthedocs.io/
|
|
27
28
|
Project-URL: Repository, https://github.com/iluvcapra/pycmx.git
|
|
28
29
|
Project-URL: Tracker, https://github.com/iluvcapra/pycmx/issues
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Provides-Extra: doc
|
|
29
32
|
Description-Content-Type: text/markdown
|
|
30
33
|
|
|
31
34
|
[](https://pycmx.readthedocs.io/en/latest/?badge=latest)   [](https://pypi.org/project/pycmx/) 
|
|
@@ -35,17 +38,22 @@ Description-Content-Type: text/markdown
|
|
|
35
38
|
|
|
36
39
|
# pycmx
|
|
37
40
|
|
|
38
|
-
The `pycmx` package
|
|
39
|
-
its most most common variations.
|
|
41
|
+
The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
|
|
40
42
|
|
|
41
43
|
## Features
|
|
42
44
|
|
|
43
|
-
* The major variations of the CMX 3600: the standard, "File32", "File128" and
|
|
45
|
+
* The major variations of the CMX 3600: the standard, "File32", "File128" and
|
|
44
46
|
long Adobe Premiere event numbers are automatically detected and properly
|
|
45
|
-
read.
|
|
47
|
+
read. Event number field and source name field sizes are determined
|
|
48
|
+
dynamically for each statement for a high level of compliance at the expense
|
|
49
|
+
of strictness.
|
|
50
|
+
* A more relaxed "tolerant" mode allows parsing of an EDL file where columns
|
|
51
|
+
use non-standard widths.
|
|
46
52
|
* Preserves relationship between events and individual edits/clips.
|
|
47
53
|
* Remark or comment fields with common recognized forms are read and
|
|
48
54
|
available to the client, including clip name and source file data.
|
|
55
|
+
* [ASC CDL][asc] and FRMC/VFX framecount statements are parsed and
|
|
56
|
+
decoded.
|
|
49
57
|
* Symbolically decodes transitions and audio channels.
|
|
50
58
|
* Does not parse or validate timecodes, does not enforce framerates, does not
|
|
51
59
|
parameterize timecode or framerates in any way. This makes the parser more
|
|
@@ -55,12 +63,14 @@ its most most common variations.
|
|
|
55
63
|
list and give the client the ability to extend the package with their own
|
|
56
64
|
parsing code.
|
|
57
65
|
|
|
66
|
+
[asc]: https://en.wikipedia.org/wiki/ASC_CDL
|
|
67
|
+
|
|
58
68
|
## Usage
|
|
59
69
|
|
|
60
70
|
### Opening and Parsing EDL Files
|
|
61
71
|
```
|
|
62
72
|
>>> import pycmx
|
|
63
|
-
>>> with open("tests/edls/TEST.edl") as f
|
|
73
|
+
>>> with open("tests/edls/TEST.edl") as f:
|
|
64
74
|
... edl = pycmx.parse_cmx3600(f)
|
|
65
75
|
...
|
|
66
76
|
>>> edl.title
|
|
@@ -115,4 +125,3 @@ Audio channel 7 is present
|
|
|
115
125
|
>>> events[2].edits[0].channels.video
|
|
116
126
|
False
|
|
117
127
|
```
|
|
118
|
-
|
|
@@ -5,17 +5,22 @@
|
|
|
5
5
|
|
|
6
6
|
# pycmx
|
|
7
7
|
|
|
8
|
-
The `pycmx` package
|
|
9
|
-
its most most common variations.
|
|
8
|
+
The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
|
|
10
9
|
|
|
11
10
|
## Features
|
|
12
11
|
|
|
13
|
-
* The major variations of the CMX 3600: the standard, "File32", "File128" and
|
|
12
|
+
* The major variations of the CMX 3600: the standard, "File32", "File128" and
|
|
14
13
|
long Adobe Premiere event numbers are automatically detected and properly
|
|
15
|
-
read.
|
|
14
|
+
read. Event number field and source name field sizes are determined
|
|
15
|
+
dynamically for each statement for a high level of compliance at the expense
|
|
16
|
+
of strictness.
|
|
17
|
+
* A more relaxed "tolerant" mode allows parsing of an EDL file where columns
|
|
18
|
+
use non-standard widths.
|
|
16
19
|
* Preserves relationship between events and individual edits/clips.
|
|
17
20
|
* Remark or comment fields with common recognized forms are read and
|
|
18
21
|
available to the client, including clip name and source file data.
|
|
22
|
+
* [ASC CDL][asc] and FRMC/VFX framecount statements are parsed and
|
|
23
|
+
decoded.
|
|
19
24
|
* Symbolically decodes transitions and audio channels.
|
|
20
25
|
* Does not parse or validate timecodes, does not enforce framerates, does not
|
|
21
26
|
parameterize timecode or framerates in any way. This makes the parser more
|
|
@@ -25,12 +30,14 @@ its most most common variations.
|
|
|
25
30
|
list and give the client the ability to extend the package with their own
|
|
26
31
|
parsing code.
|
|
27
32
|
|
|
33
|
+
[asc]: https://en.wikipedia.org/wiki/ASC_CDL
|
|
34
|
+
|
|
28
35
|
## Usage
|
|
29
36
|
|
|
30
37
|
### Opening and Parsing EDL Files
|
|
31
38
|
```
|
|
32
39
|
>>> import pycmx
|
|
33
|
-
>>> with open("tests/edls/TEST.edl") as f
|
|
40
|
+
>>> with open("tests/edls/TEST.edl") as f:
|
|
34
41
|
... edl = pycmx.parse_cmx3600(f)
|
|
35
42
|
...
|
|
36
43
|
>>> edl.title
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pycmx"
|
|
3
|
+
version = "1.5.0"
|
|
4
|
+
description = "Python CMX 3600 Edit Decision List Parser"
|
|
5
|
+
authors = [{name = "Jamie Hardt", email= "jamiehardt@me.com"}]
|
|
6
|
+
license-files = ["LICENSE"]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
keywords = [
|
|
9
|
+
'parser',
|
|
10
|
+
'film',
|
|
11
|
+
'broadcast'
|
|
12
|
+
]
|
|
13
|
+
requires-python = '>3.8'
|
|
14
|
+
classifiers = [
|
|
15
|
+
'Development Status :: 5 - Production/Stable',
|
|
16
|
+
'License :: OSI Approved :: MIT License',
|
|
17
|
+
'Topic :: Multimedia',
|
|
18
|
+
'Topic :: Multimedia :: Video',
|
|
19
|
+
'Topic :: Text Processing',
|
|
20
|
+
'Programming Language :: Python :: 3.8',
|
|
21
|
+
'Programming Language :: Python :: 3.9',
|
|
22
|
+
'Programming Language :: Python :: 3.10',
|
|
23
|
+
'Programming Language :: Python :: 3.11',
|
|
24
|
+
'Programming Language :: Python :: 3.12',
|
|
25
|
+
'Programming Language :: Python :: 3.13',
|
|
26
|
+
'Programming Language :: Python :: 3.14'
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
doc = [
|
|
31
|
+
'sphinx >= 5.3.0',
|
|
32
|
+
'sphinx_rtd_theme >= 1.1.1',
|
|
33
|
+
]
|
|
34
|
+
dev = [
|
|
35
|
+
'pytest',
|
|
36
|
+
'ruff>=0.14.10'
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/iluvcapra/pycmx"
|
|
41
|
+
Documentation = "https://pycmx.readthedocs.io/"
|
|
42
|
+
Repository = "https://github.com/iluvcapra/pycmx.git"
|
|
43
|
+
Tracker = "https://github.com/iluvcapra/pycmx/issues"
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = ['ruff', 'pytest']
|
|
47
|
+
doc = ['sphinx', 'sphinx_rtd_theme']
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
[tool.pyright]
|
|
51
|
+
typeCheckingMode = "basic"
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 88
|
|
55
|
+
indent-width = 4
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint]
|
|
58
|
+
select = ["E", "F", "W"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.format]
|
|
61
|
+
docstring-code-line-length = 88
|
|
62
|
+
|
|
63
|
+
# [tool.pylint]
|
|
64
|
+
# max-line-length = 88
|
|
65
|
+
# disable = [
|
|
66
|
+
# "C0103", # (invalid-name)
|
|
67
|
+
# "C0114", # (missing-module-docstring)
|
|
68
|
+
# "C0115", # (missing-class-docstring)
|
|
69
|
+
# "C0116", # (missing-function-docstring)
|
|
70
|
+
# "R0903", # (too-few-public-methods)
|
|
71
|
+
# "R0913", # (too-many-arguments)
|
|
72
|
+
# "W0105", # (pointless-string-statement)
|
|
73
|
+
# ]
|
|
74
|
+
#
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
[build-system]
|
|
79
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
80
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# pycmx
|
|
2
|
+
# (c) 2025 Jamie Hardt
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Generic, NamedTuple, TypeVar
|
|
6
|
+
|
|
7
|
+
T = TypeVar('T')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Rgb(Generic[T]):
|
|
12
|
+
"""
|
|
13
|
+
A tuple of three `T`s, where each is the respective red, green and blue
|
|
14
|
+
values of interest.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
red: T # : Red component
|
|
18
|
+
green: T # : Green component
|
|
19
|
+
blue: T # : Blue component
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class AscSopComponents(Generic[T]):
|
|
24
|
+
"""
|
|
25
|
+
Fields in an ASC SOP (Slope-Offset-Power) color transfer function
|
|
26
|
+
statement.
|
|
27
|
+
|
|
28
|
+
The ASC SOP is a transfer function of the form:
|
|
29
|
+
|
|
30
|
+
:math:`y_{color} = (ax_{color} + b)^p`
|
|
31
|
+
|
|
32
|
+
for each color component the source, where the `slope` is `a`, `offset`
|
|
33
|
+
is `b` and `power` is `p`.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
slope: Rgb[T] # : The linear/slope component `a`
|
|
37
|
+
offset: Rgb[T] # : The constant/offset component `b`
|
|
38
|
+
power: Rgb[T] # : The exponential/power component `p`
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FramecountTriple(NamedTuple):
|
|
42
|
+
"""
|
|
43
|
+
Fields in an FRMC statement
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
start: int
|
|
47
|
+
end: int
|
|
48
|
+
duration: int
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# pycmx
|
|
2
|
+
# (c) 2018-2025 Jamie Hardt
|
|
3
|
+
|
|
4
|
+
from .cdl import AscSopComponents, FramecountTriple
|
|
5
|
+
from .statements import (
|
|
6
|
+
StmtCdlSat,
|
|
7
|
+
StmtCdlSop,
|
|
8
|
+
StmtFrmc,
|
|
9
|
+
StmtEvent,
|
|
10
|
+
StmtAudioExt,
|
|
11
|
+
StmtClipName,
|
|
12
|
+
StmtSourceFile,
|
|
13
|
+
StmtEffectsName,
|
|
14
|
+
)
|
|
15
|
+
from .transition import Transition
|
|
16
|
+
from .channel_map import ChannelMap
|
|
17
|
+
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Edit:
|
|
22
|
+
"""
|
|
23
|
+
An individual source-to-record operation, with a source roll, source and
|
|
24
|
+
recorder timecode in and out, a transition and channels.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
edit_statement: StmtEvent,
|
|
30
|
+
audio_ext_statement: Optional[StmtAudioExt],
|
|
31
|
+
clip_name_statement: Optional[StmtClipName],
|
|
32
|
+
source_file_statement: Optional[StmtSourceFile],
|
|
33
|
+
trans_name_statement: Optional[StmtEffectsName] = None,
|
|
34
|
+
asc_sop_statement: Optional[StmtCdlSop] = None,
|
|
35
|
+
asc_sat_statement: Optional[StmtCdlSat] = None,
|
|
36
|
+
frmc_statement: Optional[StmtFrmc] = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
self._edit_statement: StmtEvent = edit_statement
|
|
39
|
+
self._audio_ext: Optional[StmtAudioExt] = audio_ext_statement
|
|
40
|
+
self._clip_name_statement: Optional[StmtClipName] = clip_name_statement
|
|
41
|
+
self._source_file_statement: Optional[StmtSourceFile] = \
|
|
42
|
+
source_file_statement
|
|
43
|
+
self._trans_name_statement: Optional[StmtEffectsName] = \
|
|
44
|
+
trans_name_statement
|
|
45
|
+
self._asc_sop_statement: Optional[StmtCdlSop] = asc_sop_statement
|
|
46
|
+
self._asc_sat_statement: Optional[StmtCdlSat] = asc_sat_statement
|
|
47
|
+
self._frmc_statement: Optional[StmtFrmc] = frmc_statement
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def line_number(self) -> int:
|
|
51
|
+
"""
|
|
52
|
+
Get the line number for the "standard form" statement associated with
|
|
53
|
+
this edit. Line numbers a zero-indexed, such that the "TITLE:" record
|
|
54
|
+
is line zero.
|
|
55
|
+
"""
|
|
56
|
+
return self._edit_statement.line_number
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def channels(self) -> ChannelMap:
|
|
60
|
+
"""
|
|
61
|
+
Get the :obj:`ChannelMap` object associated with this Edit.
|
|
62
|
+
"""
|
|
63
|
+
cm = ChannelMap()
|
|
64
|
+
cm._append_event(self._edit_statement.channels)
|
|
65
|
+
if self._audio_ext is not None:
|
|
66
|
+
cm._append_ext(self._audio_ext)
|
|
67
|
+
return cm
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def transition(self) -> Transition:
|
|
71
|
+
"""
|
|
72
|
+
Get the :obj:`Transition` that initiates this edit.
|
|
73
|
+
"""
|
|
74
|
+
if self._trans_name_statement:
|
|
75
|
+
return Transition(
|
|
76
|
+
self._edit_statement.trans,
|
|
77
|
+
self._edit_statement.trans_op,
|
|
78
|
+
self._trans_name_statement.name,
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
return Transition(
|
|
82
|
+
self._edit_statement.trans, self._edit_statement.trans_op, None
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def source_in(self) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Get the source in timecode.
|
|
89
|
+
"""
|
|
90
|
+
return self._edit_statement.source_in
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def source_out(self) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Get the source out timecode.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
return self._edit_statement.source_out
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def record_in(self) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Get the record in timecode.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
return self._edit_statement.record_in
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def record_out(self) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Get the record out timecode.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
return self._edit_statement.record_out
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def source(self) -> str:
|
|
118
|
+
"""
|
|
119
|
+
Get the source column. This is the 8, 32 or 128-character string on the
|
|
120
|
+
event record line, this usually references the tape name of the source.
|
|
121
|
+
"""
|
|
122
|
+
return self._edit_statement.source
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def black(self) -> bool:
|
|
126
|
+
"""
|
|
127
|
+
The source field for thie edit was "BL". Black video or silence should
|
|
128
|
+
be used as the source for this event.
|
|
129
|
+
"""
|
|
130
|
+
return self.source == "BL"
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def aux_source(self) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
The source field for this edit was "AX". An auxiliary source is the
|
|
136
|
+
source for this event.
|
|
137
|
+
"""
|
|
138
|
+
return self.source == "AX"
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def source_file(self) -> Optional[str]:
|
|
142
|
+
"""
|
|
143
|
+
Get the source file, as attested by a "* SOURCE FILE" remark on the
|
|
144
|
+
EDL. This will return None if the information is not present.
|
|
145
|
+
"""
|
|
146
|
+
if self._source_file_statement is None:
|
|
147
|
+
return None
|
|
148
|
+
else:
|
|
149
|
+
return self._source_file_statement.filename
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def clip_name(self) -> Optional[str]:
|
|
153
|
+
"""
|
|
154
|
+
Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP
|
|
155
|
+
NAME" remark on the EDL. This will return None if the information is
|
|
156
|
+
not present.
|
|
157
|
+
"""
|
|
158
|
+
if self._clip_name_statement is None:
|
|
159
|
+
return None
|
|
160
|
+
else:
|
|
161
|
+
return self._clip_name_statement.name
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def asc_sop(self) -> Optional[AscSopComponents[float]]:
|
|
165
|
+
"""
|
|
166
|
+
Get ASC CDL Slope-Offset-Power color transfer function for the edit,
|
|
167
|
+
if present. The ASC SOP is a transfer function of the form:
|
|
168
|
+
|
|
169
|
+
:math:`y = (ax + b)^p`
|
|
170
|
+
|
|
171
|
+
for each color component the source, where the `slope` is `a`, `offset`
|
|
172
|
+
is `b` and `power` is `p`.
|
|
173
|
+
"""
|
|
174
|
+
if self._asc_sop_statement is None:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
return self._asc_sop_statement.cdl_sop
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def asc_sop_raw(self) -> Optional[str]:
|
|
181
|
+
"""
|
|
182
|
+
ASC CDL Slope-Offset-Power statement raw line.
|
|
183
|
+
"""
|
|
184
|
+
if self._asc_sop_statement is None:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
return self._asc_sop_statement.line
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def asc_sat(self) -> Optional[float]:
|
|
191
|
+
"""
|
|
192
|
+
Get ASC CDL saturation value for clip, if present.
|
|
193
|
+
"""
|
|
194
|
+
if self._asc_sat_statement is None:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
return self._asc_sat_statement.value
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def framecounts(self) -> Optional[FramecountTriple]:
|
|
201
|
+
"""
|
|
202
|
+
Get frame count offset data, if it exists. If an FRMC statement exists
|
|
203
|
+
in the EDL for the event it will give an integer frame count for the
|
|
204
|
+
edit's source in and out times.
|
|
205
|
+
"""
|
|
206
|
+
if not self._frmc_statement:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
return FramecountTriple(
|
|
210
|
+
start=self._frmc_statement.start,
|
|
211
|
+
end=self._frmc_statement.end,
|
|
212
|
+
duration=self._frmc_statement.duration,
|
|
213
|
+
)
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
# pycmx
|
|
2
|
-
# (c) 2018 Jamie Hardt
|
|
2
|
+
# (c) 2018-2025 Jamie Hardt
|
|
3
3
|
|
|
4
|
-
from .
|
|
5
|
-
|
|
4
|
+
from .statements import (StmtCorruptRemark, StmtTitle, StmtEvent,
|
|
5
|
+
StmtUnrecognized, StmtSourceUMID)
|
|
6
6
|
from .event import Event
|
|
7
7
|
from .channel_map import ChannelMap
|
|
8
8
|
|
|
9
|
-
from typing import Generator
|
|
9
|
+
from typing import Any, Generator
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class EditList:
|
|
13
13
|
"""
|
|
14
14
|
Represents an entire edit decision list as returned by
|
|
15
|
-
:func:`~pycmx.parse_cmx3600()`.
|
|
15
|
+
:func:`~pycmx.parse_cmx_events.parse_cmx3600()`.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
def __init__(self, statements):
|
|
19
|
-
self.title_statement = statements[0]
|
|
18
|
+
def __init__(self, statements: list):
|
|
19
|
+
self.title_statement: StmtTitle = statements[0]
|
|
20
20
|
self.event_statements = statements[1:]
|
|
21
21
|
|
|
22
22
|
@property
|
|
@@ -31,11 +31,11 @@ class EditList:
|
|
|
31
31
|
(s for s in self.event_statements if type(s) is StmtEvent), None)
|
|
32
32
|
|
|
33
33
|
if first_event:
|
|
34
|
-
if first_event.
|
|
34
|
+
if first_event.source_field_size == 8:
|
|
35
35
|
return '3600'
|
|
36
|
-
elif first_event.
|
|
36
|
+
elif first_event.source_field_size == 32:
|
|
37
37
|
return 'File32'
|
|
38
|
-
elif first_event.
|
|
38
|
+
elif first_event.source_field_size == 128:
|
|
39
39
|
return 'File128'
|
|
40
40
|
else:
|
|
41
41
|
return 'unknown'
|
|
@@ -63,13 +63,16 @@ class EditList:
|
|
|
63
63
|
return self.title_statement.title
|
|
64
64
|
|
|
65
65
|
@property
|
|
66
|
-
def unrecognized_statements(self) -> Generator[
|
|
67
|
-
None, None]:
|
|
66
|
+
def unrecognized_statements(self) -> Generator[Any, None, None]:
|
|
68
67
|
"""
|
|
69
|
-
A generator for all the unrecognized statements
|
|
68
|
+
A generator for all the unrecognized statements and
|
|
69
|
+
corrupt remarks in the list.
|
|
70
|
+
|
|
71
|
+
:yields: either a :class:`StmtUnrecognized` or
|
|
72
|
+
:class:`StmtCorruptRemark`
|
|
70
73
|
"""
|
|
71
74
|
for s in self.event_statements:
|
|
72
|
-
if type(s) is StmtUnrecognized:
|
|
75
|
+
if type(s) is StmtUnrecognized or type(s) in StmtCorruptRemark:
|
|
73
76
|
yield s
|
|
74
77
|
|
|
75
78
|
@property
|
|
@@ -90,8 +93,6 @@ class EditList:
|
|
|
90
93
|
else:
|
|
91
94
|
event_statements.append(stmt)
|
|
92
95
|
|
|
93
|
-
elif type(stmt) is StmtSourceUMID:
|
|
94
|
-
break
|
|
95
96
|
else:
|
|
96
97
|
event_statements.append(stmt)
|
|
97
98
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# pycmx
|
|
2
|
-
# (c) 2023 Jamie Hardt
|
|
2
|
+
# (c) 2023-2025 Jamie Hardt
|
|
3
3
|
|
|
4
|
-
from .
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
from .statements import (StmtFrmc, StmtEvent, StmtClipName, StmtSourceFile,
|
|
5
|
+
StmtAudioExt, StmtUnrecognized, StmtEffectsName,
|
|
6
|
+
StmtCdlSop, StmtCdlSat)
|
|
7
7
|
from .edit import Edit
|
|
8
8
|
|
|
9
9
|
from typing import List, Generator, Optional, Tuple, Any
|
|
@@ -31,12 +31,27 @@ class Event:
|
|
|
31
31
|
will have multiple edits when a dissolve, wipe or key transition needs
|
|
32
32
|
to be performed.
|
|
33
33
|
"""
|
|
34
|
+
|
|
35
|
+
# FTR this is a totall bonkers way of doing this, I wrote this when
|
|
36
|
+
# I was still learning Python and I'm sure there's easier ways to do
|
|
37
|
+
# it. The job is complicated because multiple edits can occur in one
|
|
38
|
+
# event and then other statements can modify the event in different
|
|
39
|
+
# ways.
|
|
40
|
+
|
|
34
41
|
edits_audio = list(self._statements_with_audio_ext())
|
|
35
42
|
clip_names = self._clip_name_statements()
|
|
36
43
|
source_files = self._source_file_statements()
|
|
37
44
|
|
|
45
|
+
# We first get the edit events combined with their extra audio
|
|
46
|
+
# channel statements, if any.
|
|
47
|
+
|
|
48
|
+
# The list the_zip contains one element for each initialization
|
|
49
|
+
# parameter in Edit()
|
|
38
50
|
the_zip: List[List[Any]] = [edits_audio]
|
|
39
51
|
|
|
52
|
+
# If there are two Clip Name statements and two edits, we look for
|
|
53
|
+
# "FROM" and "TO" clip name lines. Otherwise we just look for on
|
|
54
|
+
# each per edit.
|
|
40
55
|
if len(edits_audio) == 2:
|
|
41
56
|
start_name: Optional[StmtClipName] = None
|
|
42
57
|
end_name: Optional[StmtClipName] = None
|
|
@@ -54,6 +69,10 @@ class Event:
|
|
|
54
69
|
else:
|
|
55
70
|
the_zip.append([None] * len(edits_audio))
|
|
56
71
|
|
|
72
|
+
# if there's one source file statemnent per clip, we allocate them to
|
|
73
|
+
# each edit in order. Otherwise if there's only one, we assign the one
|
|
74
|
+
# to all the edits. If there's no source_file statements, we provide
|
|
75
|
+
# None.
|
|
57
76
|
if len(edits_audio) == len(source_files):
|
|
58
77
|
the_zip.append(source_files)
|
|
59
78
|
elif len(source_files) == 1:
|
|
@@ -61,7 +80,7 @@ class Event:
|
|
|
61
80
|
else:
|
|
62
81
|
the_zip.append([None] * len(edits_audio))
|
|
63
82
|
|
|
64
|
-
# attach
|
|
83
|
+
# attach effects name to last event
|
|
65
84
|
try:
|
|
66
85
|
trans_statement = self._trans_name_statements()[0]
|
|
67
86
|
trans_names: List[Optional[Any]] = [None] * (len(edits_audio) - 1)
|
|
@@ -74,7 +93,10 @@ class Event:
|
|
|
74
93
|
audio_ext_statement=e1[1],
|
|
75
94
|
clip_name_statement=n1,
|
|
76
95
|
source_file_statement=s1,
|
|
77
|
-
trans_name_statement=u1
|
|
96
|
+
trans_name_statement=u1,
|
|
97
|
+
asc_sop_statement=self._asc_sop_statement(),
|
|
98
|
+
asc_sat_statement=self._asc_sat_statement(),
|
|
99
|
+
frmc_statement=self._frmc_statement())
|
|
78
100
|
for (e1, n1, s1, u1) in zip(*the_zip)]
|
|
79
101
|
|
|
80
102
|
@property
|
|
@@ -101,8 +123,24 @@ class Event:
|
|
|
101
123
|
|
|
102
124
|
def _statements_with_audio_ext(self) -> Generator[
|
|
103
125
|
Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
126
|
+
|
|
127
|
+
if len(self.statements) == 1 and type(self.statements[0]) is StmtEvent:
|
|
128
|
+
yield (self.statements[0], None)
|
|
129
|
+
|
|
130
|
+
else:
|
|
131
|
+
for (s1, s2) in zip(self.statements, self.statements[1:]):
|
|
132
|
+
if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
|
|
133
|
+
yield (s1, s2)
|
|
134
|
+
elif type(s1) is StmtEvent:
|
|
135
|
+
yield (s1, None)
|
|
136
|
+
|
|
137
|
+
def _asc_sop_statement(self) -> Optional[StmtCdlSop]:
|
|
138
|
+
return next((s for s in self.statements if type(s) is StmtCdlSop),
|
|
139
|
+
None)
|
|
140
|
+
|
|
141
|
+
def _asc_sat_statement(self) -> Optional[StmtCdlSat]:
|
|
142
|
+
return next((s for s in self.statements if type(s) is StmtCdlSat),
|
|
143
|
+
None)
|
|
144
|
+
|
|
145
|
+
def _frmc_statement(self) -> Optional[StmtFrmc]:
|
|
146
|
+
return next((s for s in self.statements if type(s) is StmtFrmc), None)
|