pycmx 1.3.0__py3-none-any.whl → 1.4.0__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.
- pycmx/cdl.py +48 -0
- pycmx/channel_map.py +1 -1
- pycmx/edit.py +114 -33
- pycmx/edit_list.py +15 -14
- pycmx/event.py +19 -5
- pycmx/parse_cmx_events.py +2 -4
- pycmx/parse_cmx_statements.py +105 -75
- pycmx/statements.py +104 -0
- pycmx/util.py +1 -1
- {pycmx-1.3.0.dist-info → pycmx-1.4.0.dist-info}/METADATA +14 -7
- pycmx-1.4.0.dist-info/RECORD +15 -0
- {pycmx-1.3.0.dist-info → pycmx-1.4.0.dist-info}/WHEEL +1 -1
- pycmx-1.3.0.dist-info/RECORD +0 -13
- {pycmx-1.3.0.dist-info → pycmx-1.4.0.dist-info/licenses}/LICENSE +0 -0
pycmx/cdl.py
ADDED
|
@@ -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
|
pycmx/channel_map.py
CHANGED
pycmx/edit.py
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
# pycmx
|
|
2
|
-
# (c) 2018 Jamie Hardt
|
|
3
|
-
|
|
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
|
+
)
|
|
4
15
|
from .transition import Transition
|
|
5
16
|
from .channel_map import ChannelMap
|
|
6
|
-
# from .parse_cmx_statements import StmtEffectsName
|
|
7
17
|
|
|
8
18
|
from typing import Optional
|
|
9
19
|
|
|
@@ -14,14 +24,28 @@ class Edit:
|
|
|
14
24
|
recorder timecode in and out, a transition and channels.
|
|
15
25
|
"""
|
|
16
26
|
|
|
17
|
-
def __init__(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
# Assigning types for the attributes explicitly
|
|
39
|
+
self._edit_statement: StmtEvent = edit_statement
|
|
40
|
+
self._audio_ext: Optional[StmtAudioExt] = audio_ext_statement
|
|
41
|
+
self._clip_name_statement: Optional[StmtClipName] = clip_name_statement
|
|
42
|
+
self._source_file_statement: Optional[StmtSourceFile] = \
|
|
43
|
+
source_file_statement
|
|
44
|
+
self._trans_name_statement: Optional[StmtEffectsName] = \
|
|
45
|
+
trans_name_statement
|
|
46
|
+
self._asc_sop_statement: Optional[StmtCdlSop] = asc_sop_statement
|
|
47
|
+
self._asc_sat_statement: Optional[StmtCdlSat] = asc_sat_statement
|
|
48
|
+
self._frmc_statement: Optional[StmtFrmc] = frmc_statement
|
|
25
49
|
|
|
26
50
|
@property
|
|
27
51
|
def line_number(self) -> int:
|
|
@@ -30,7 +54,7 @@ class Edit:
|
|
|
30
54
|
this edit. Line numbers a zero-indexed, such that the
|
|
31
55
|
"TITLE:" record is line zero.
|
|
32
56
|
"""
|
|
33
|
-
return self.
|
|
57
|
+
return self._edit_statement.line_number
|
|
34
58
|
|
|
35
59
|
@property
|
|
36
60
|
def channels(self) -> ChannelMap:
|
|
@@ -38,30 +62,33 @@ class Edit:
|
|
|
38
62
|
Get the :obj:`ChannelMap` object associated with this Edit.
|
|
39
63
|
"""
|
|
40
64
|
cm = ChannelMap()
|
|
41
|
-
cm._append_event(self.
|
|
42
|
-
if self.
|
|
43
|
-
cm._append_ext(self.
|
|
65
|
+
cm._append_event(self._edit_statement.channels)
|
|
66
|
+
if self._audio_ext is not None:
|
|
67
|
+
cm._append_ext(self._audio_ext)
|
|
44
68
|
return cm
|
|
45
69
|
|
|
46
70
|
@property
|
|
47
71
|
def transition(self) -> Transition:
|
|
48
72
|
"""
|
|
49
|
-
Get the :obj:`Transition`
|
|
73
|
+
Get the :obj:`Transition` that initiates this edit.
|
|
50
74
|
"""
|
|
51
|
-
if self.
|
|
52
|
-
return Transition(
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
if self._trans_name_statement:
|
|
76
|
+
return Transition(
|
|
77
|
+
self._edit_statement.trans,
|
|
78
|
+
self._edit_statement.trans_op,
|
|
79
|
+
self._trans_name_statement.name,
|
|
80
|
+
)
|
|
55
81
|
else:
|
|
56
|
-
return Transition(
|
|
57
|
-
|
|
82
|
+
return Transition(
|
|
83
|
+
self._edit_statement.trans, self._edit_statement.trans_op, None
|
|
84
|
+
)
|
|
58
85
|
|
|
59
86
|
@property
|
|
60
87
|
def source_in(self) -> str:
|
|
61
88
|
"""
|
|
62
89
|
Get the source in timecode.
|
|
63
90
|
"""
|
|
64
|
-
return self.
|
|
91
|
+
return self._edit_statement.source_in
|
|
65
92
|
|
|
66
93
|
@property
|
|
67
94
|
def source_out(self) -> str:
|
|
@@ -69,7 +96,7 @@ class Edit:
|
|
|
69
96
|
Get the source out timecode.
|
|
70
97
|
"""
|
|
71
98
|
|
|
72
|
-
return self.
|
|
99
|
+
return self._edit_statement.source_out
|
|
73
100
|
|
|
74
101
|
@property
|
|
75
102
|
def record_in(self) -> str:
|
|
@@ -77,7 +104,7 @@ class Edit:
|
|
|
77
104
|
Get the record in timecode.
|
|
78
105
|
"""
|
|
79
106
|
|
|
80
|
-
return self.
|
|
107
|
+
return self._edit_statement.record_in
|
|
81
108
|
|
|
82
109
|
@property
|
|
83
110
|
def record_out(self) -> str:
|
|
@@ -85,7 +112,7 @@ class Edit:
|
|
|
85
112
|
Get the record out timecode.
|
|
86
113
|
"""
|
|
87
114
|
|
|
88
|
-
return self.
|
|
115
|
+
return self._edit_statement.record_out
|
|
89
116
|
|
|
90
117
|
@property
|
|
91
118
|
def source(self) -> str:
|
|
@@ -93,19 +120,21 @@ class Edit:
|
|
|
93
120
|
Get the source column. This is the 8, 32 or 128-character string on the
|
|
94
121
|
event record line, this usually references the tape name of the source.
|
|
95
122
|
"""
|
|
96
|
-
return self.
|
|
123
|
+
return self._edit_statement.source
|
|
97
124
|
|
|
98
125
|
@property
|
|
99
126
|
def black(self) -> bool:
|
|
100
127
|
"""
|
|
101
|
-
|
|
128
|
+
The source field for thie edit was "BL". Black video or silence should
|
|
129
|
+
be used as the source for this event.
|
|
102
130
|
"""
|
|
103
131
|
return self.source == "BL"
|
|
104
132
|
|
|
105
133
|
@property
|
|
106
134
|
def aux_source(self) -> bool:
|
|
107
135
|
"""
|
|
108
|
-
An auxiliary source is the
|
|
136
|
+
The source field for this edit was "AX". An auxiliary source is the
|
|
137
|
+
source for this event.
|
|
109
138
|
"""
|
|
110
139
|
return self.source == "AX"
|
|
111
140
|
|
|
@@ -115,10 +144,10 @@ class Edit:
|
|
|
115
144
|
Get the source file, as attested by a "* SOURCE FILE" remark on the
|
|
116
145
|
EDL. This will return None if the information is not present.
|
|
117
146
|
"""
|
|
118
|
-
if self.
|
|
147
|
+
if self._source_file_statement is None:
|
|
119
148
|
return None
|
|
120
149
|
else:
|
|
121
|
-
return self.
|
|
150
|
+
return self._source_file_statement.filename
|
|
122
151
|
|
|
123
152
|
@property
|
|
124
153
|
def clip_name(self) -> Optional[str]:
|
|
@@ -127,7 +156,59 @@ class Edit:
|
|
|
127
156
|
NAME" remark on the EDL. This will return None if the information is
|
|
128
157
|
not present.
|
|
129
158
|
"""
|
|
130
|
-
if self.
|
|
159
|
+
if self._clip_name_statement is None:
|
|
131
160
|
return None
|
|
132
161
|
else:
|
|
133
|
-
return self.
|
|
162
|
+
return self._clip_name_statement.name
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def asc_sop(self) -> Optional[AscSopComponents[float]]:
|
|
166
|
+
"""
|
|
167
|
+
Get ASC CDL Slope-Offset-Power color transfer function for the edit,
|
|
168
|
+
if present. The ASC SOP is a transfer function of the form:
|
|
169
|
+
|
|
170
|
+
:math:`y = (ax + b)^p`
|
|
171
|
+
|
|
172
|
+
for each color component the source, where the `slope` is `a`, `offset`
|
|
173
|
+
is `b` and `power` is `p`.
|
|
174
|
+
"""
|
|
175
|
+
if self._asc_sop_statement is None:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
return self._asc_sop_statement.cdl_sop
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def asc_sop_raw(self) -> Optional[str]:
|
|
182
|
+
"""
|
|
183
|
+
ASC CDL Slope-Offset-Power statement raw line
|
|
184
|
+
"""
|
|
185
|
+
if self._asc_sop_statement is None:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
return self._asc_sop_statement.line
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def asc_sat(self) -> Optional[float]:
|
|
192
|
+
"""
|
|
193
|
+
Get ASC CDL saturation value for clip, if present
|
|
194
|
+
"""
|
|
195
|
+
if self._asc_sat_statement is None:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
return self._asc_sat_statement.value
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def framecounts(self) -> Optional[FramecountTriple]:
|
|
202
|
+
"""
|
|
203
|
+
Get frame count offset data, if it exists. If an FRMC statement exists
|
|
204
|
+
in the EDL for the event it will give an integer frame count for the
|
|
205
|
+
edit's source in and out times.
|
|
206
|
+
"""
|
|
207
|
+
if not self._frmc_statement:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
return FramecountTriple(
|
|
211
|
+
start=self._frmc_statement.start,
|
|
212
|
+
end=self._frmc_statement.end,
|
|
213
|
+
duration=self._frmc_statement.duration,
|
|
214
|
+
)
|
pycmx/edit_list.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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:
|
|
@@ -16,7 +16,7 @@ class EditList:
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
def __init__(self, statements):
|
|
19
|
-
self.title_statement = statements[0]
|
|
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
|
|
pycmx/event.py
CHANGED
|
@@ -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
|
|
@@ -74,7 +74,10 @@ class Event:
|
|
|
74
74
|
audio_ext_statement=e1[1],
|
|
75
75
|
clip_name_statement=n1,
|
|
76
76
|
source_file_statement=s1,
|
|
77
|
-
trans_name_statement=u1
|
|
77
|
+
trans_name_statement=u1,
|
|
78
|
+
asc_sop_statement=self._asc_sop_statement(),
|
|
79
|
+
asc_sat_statement=self._asc_sat_statement(),
|
|
80
|
+
frmc_statement=self._frmc_statement())
|
|
78
81
|
for (e1, n1, s1, u1) in zip(*the_zip)]
|
|
79
82
|
|
|
80
83
|
@property
|
|
@@ -106,3 +109,14 @@ class Event:
|
|
|
106
109
|
yield (s1, s2)
|
|
107
110
|
elif type(s1) is StmtEvent:
|
|
108
111
|
yield (s1, None)
|
|
112
|
+
|
|
113
|
+
def _asc_sop_statement(self) -> Optional[StmtCdlSop]:
|
|
114
|
+
return next((s for s in self.statements if type(s) is StmtCdlSop),
|
|
115
|
+
None)
|
|
116
|
+
|
|
117
|
+
def _asc_sat_statement(self) -> Optional[StmtCdlSat]:
|
|
118
|
+
return next((s for s in self.statements if type(s) is StmtCdlSat),
|
|
119
|
+
None)
|
|
120
|
+
|
|
121
|
+
def _frmc_statement(self) -> Optional[StmtFrmc]:
|
|
122
|
+
return next((s for s in self.statements if type(s) is StmtFrmc), None)
|
pycmx/parse_cmx_events.py
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# pycmx
|
|
2
|
-
# (c) 2018 Jamie Hardt
|
|
2
|
+
# (c) 2018-2025 Jamie Hardt
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
from typing import TextIO
|
|
5
5
|
|
|
6
6
|
from .parse_cmx_statements import (parse_cmx3600_statements)
|
|
7
7
|
from .edit_list import EditList
|
|
8
8
|
|
|
9
|
-
from typing import TextIO
|
|
10
|
-
|
|
11
9
|
|
|
12
10
|
def parse_cmx3600(f: TextIO) -> EditList:
|
|
13
11
|
"""
|
pycmx/parse_cmx_statements.py
CHANGED
|
@@ -1,30 +1,17 @@
|
|
|
1
1
|
# pycmx
|
|
2
|
-
# (c) 2018 Jamie Hardt
|
|
2
|
+
# (c) 2018-2025 Jamie Hardt
|
|
3
3
|
|
|
4
4
|
import re
|
|
5
|
-
from collections import namedtuple
|
|
6
5
|
from typing import TextIO, List
|
|
7
6
|
|
|
7
|
+
from .cdl import AscSopComponents, Rgb
|
|
8
8
|
|
|
9
|
+
from .statements import (StmtCdlSat, StmtCdlSop, StmtCorruptRemark, StmtFrmc,
|
|
10
|
+
StmtRemark, StmtTitle, StmtUnrecognized, StmtFCM,
|
|
11
|
+
StmtAudioExt, StmtClipName, StmtEffectsName,
|
|
12
|
+
StmtEvent, StmtSourceFile, StmtSplitEdit)
|
|
9
13
|
from .util import collimate
|
|
10
14
|
|
|
11
|
-
StmtTitle = namedtuple("Title", ["title", "line_number"])
|
|
12
|
-
StmtFCM = namedtuple("FCM", ["drop", "line_number"])
|
|
13
|
-
StmtEvent = namedtuple("Event", ["event", "source", "channels", "trans",
|
|
14
|
-
"trans_op", "source_in", "source_out",
|
|
15
|
-
"record_in", "record_out", "format",
|
|
16
|
-
"line_number"])
|
|
17
|
-
StmtAudioExt = namedtuple("AudioExt", ["audio3", "audio4", "line_number"])
|
|
18
|
-
StmtClipName = namedtuple("ClipName", ["name", "affect", "line_number"])
|
|
19
|
-
StmtSourceFile = namedtuple("SourceFile", ["filename", "line_number"])
|
|
20
|
-
StmtRemark = namedtuple("Remark", ["text", "line_number"])
|
|
21
|
-
StmtEffectsName = namedtuple("EffectsName", ["name", "line_number"])
|
|
22
|
-
StmtSourceUMID = namedtuple("Source", ["name", "umid", "line_number"])
|
|
23
|
-
StmtSplitEdit = namedtuple("SplitEdit", ["video", "magnitude", "line_number"])
|
|
24
|
-
StmtMotionMemory = namedtuple(
|
|
25
|
-
"MotionMemory", ["source", "fps"]) # FIXME needs more fields
|
|
26
|
-
StmtUnrecognized = namedtuple("Unrecognized", ["content", "line_number"])
|
|
27
|
-
|
|
28
15
|
|
|
29
16
|
def parse_cmx3600_statements(file: TextIO) -> List[object]:
|
|
30
17
|
"""
|
|
@@ -58,41 +45,33 @@ def _parse_cmx3600_line(line: str, line_number: int) -> object:
|
|
|
58
45
|
:param line: A single EDL line.
|
|
59
46
|
:param line_number: The index of this line in the file.
|
|
60
47
|
"""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
x_event_form_p = re.compile("^([0-9]{4,5}) ")
|
|
48
|
+
event_num_p = re.compile(r"^(\d+) ")
|
|
49
|
+
line_matcher = event_num_p.match(line)
|
|
64
50
|
|
|
65
51
|
if line.startswith("TITLE:"):
|
|
66
52
|
return _parse_title(line, line_number)
|
|
67
|
-
|
|
53
|
+
if line.startswith("FCM:"):
|
|
68
54
|
return _parse_fcm(line, line_number)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
elif (m := x_event_form_p.match(line)) is not None:
|
|
76
|
-
assert m is not None
|
|
77
|
-
event_field_length = len(m[1])
|
|
78
|
-
return _parse_columns_for_standard_form(line, event_field_length,
|
|
79
|
-
8, line_number)
|
|
80
|
-
elif short_event_num_p.match(line) is not None:
|
|
81
|
-
return _parse_standard_form(line, line_number)
|
|
82
|
-
elif line.startswith("AUD"):
|
|
55
|
+
if line_matcher is not None:
|
|
56
|
+
event_field_len = len(line_matcher.group(1))
|
|
57
|
+
source_field_len = len(line) - (event_field_len + 65)
|
|
58
|
+
return _parse_columns_for_standard_form(line, event_field_len,
|
|
59
|
+
source_field_len, line_number)
|
|
60
|
+
if line.startswith("AUD"):
|
|
83
61
|
return _parse_extended_audio_channels(line, line_number)
|
|
84
|
-
|
|
62
|
+
if line.startswith("*"):
|
|
85
63
|
return _parse_remark(line[1:].strip(), line_number)
|
|
86
|
-
|
|
64
|
+
if line.startswith(">>> SOURCE"):
|
|
87
65
|
return _parse_source_umid_statement(line, line_number)
|
|
88
|
-
|
|
66
|
+
if line.startswith("EFFECTS NAME IS"):
|
|
89
67
|
return _parse_effects_name(line, line_number)
|
|
90
|
-
|
|
68
|
+
if line.startswith("SPLIT:"):
|
|
91
69
|
return _parse_split(line, line_number)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
70
|
+
if line.startswith("M2"):
|
|
71
|
+
pass
|
|
72
|
+
# return _parse_motion_memory(line, line_number)
|
|
73
|
+
|
|
74
|
+
return _parse_unrecognized(line, line_number)
|
|
96
75
|
|
|
97
76
|
|
|
98
77
|
def _parse_title(line, line_num) -> StmtTitle:
|
|
@@ -104,29 +83,19 @@ def _parse_fcm(line, line_num) -> StmtFCM:
|
|
|
104
83
|
val = line[4:].strip()
|
|
105
84
|
if val == "DROP FRAME":
|
|
106
85
|
return StmtFCM(drop=True, line_number=line_num)
|
|
107
|
-
else:
|
|
108
|
-
return StmtFCM(drop=False, line_number=line_num)
|
|
109
|
-
|
|
110
86
|
|
|
111
|
-
|
|
112
|
-
return _parse_columns_for_standard_form(line, 6, source_field_length,
|
|
113
|
-
line_number)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _parse_standard_form(line, line_number):
|
|
117
|
-
return _parse_columns_for_standard_form(line, 3, 8, line_number)
|
|
87
|
+
return StmtFCM(drop=False, line_number=line_num)
|
|
118
88
|
|
|
119
89
|
|
|
120
90
|
def _parse_extended_audio_channels(line, line_number):
|
|
121
91
|
content = line.strip()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return StmtAudioExt(audio3=True, audio4=True, line_number=line_number)
|
|
92
|
+
audio3 = "3" in content
|
|
93
|
+
audio4 = "4" in content
|
|
94
|
+
|
|
95
|
+
if audio3 or audio4:
|
|
96
|
+
return StmtAudioExt(audio3, audio4, line_number)
|
|
128
97
|
else:
|
|
129
|
-
return StmtUnrecognized(
|
|
98
|
+
return StmtUnrecognized(line, line_number)
|
|
130
99
|
|
|
131
100
|
|
|
132
101
|
def _parse_remark(line, line_number) -> object:
|
|
@@ -139,6 +108,66 @@ def _parse_remark(line, line_number) -> object:
|
|
|
139
108
|
elif line.startswith("SOURCE FILE:"):
|
|
140
109
|
return StmtSourceFile(filename=line[12:].strip(),
|
|
141
110
|
line_number=line_number)
|
|
111
|
+
elif line.startswith("ASC_SOP"):
|
|
112
|
+
group_patterns: list[str] = re.findall(r'\((.*?)\)', line)
|
|
113
|
+
|
|
114
|
+
v1: list[list[tuple[str, str]]] = \
|
|
115
|
+
[re.findall(r'(-?\d+(\.\d+)?)', a) for a in group_patterns]
|
|
116
|
+
|
|
117
|
+
v: list[list[str]] = [[a[0] for a in b] for b in v1]
|
|
118
|
+
|
|
119
|
+
if len(v) != 3 or any([len(a) != 3 for a in v]):
|
|
120
|
+
return StmtRemark(line, line_number)
|
|
121
|
+
|
|
122
|
+
else:
|
|
123
|
+
try:
|
|
124
|
+
return StmtCdlSop(line=line,
|
|
125
|
+
cdl_sop=AscSopComponents(
|
|
126
|
+
slope=Rgb(red=float(v[0][0]),
|
|
127
|
+
green=float(v[0][1]),
|
|
128
|
+
blue=float(v[0][2])),
|
|
129
|
+
offset=Rgb(red=float(v[1][0]),
|
|
130
|
+
green=float(v[1][1]),
|
|
131
|
+
blue=float(v[1][2])),
|
|
132
|
+
power=Rgb(red=float(v[2][0]),
|
|
133
|
+
green=float(v[2][1]),
|
|
134
|
+
blue=float(v[2][2]))
|
|
135
|
+
),
|
|
136
|
+
line_number=line_number)
|
|
137
|
+
|
|
138
|
+
except ValueError as e:
|
|
139
|
+
return StmtCorruptRemark('ASC_SOP', e, line_number)
|
|
140
|
+
|
|
141
|
+
elif line.startswith("ASC_SAT"):
|
|
142
|
+
value = re.findall(r'(-?\d+(\.\d+)?)', line)
|
|
143
|
+
|
|
144
|
+
if len(value) != 1:
|
|
145
|
+
return StmtRemark(line, line_number)
|
|
146
|
+
|
|
147
|
+
else:
|
|
148
|
+
try:
|
|
149
|
+
return StmtCdlSat(value=float(value[0][0]),
|
|
150
|
+
line_number=line_number)
|
|
151
|
+
|
|
152
|
+
except ValueError as e:
|
|
153
|
+
return StmtCorruptRemark('ASC_SAT', e, line_number)
|
|
154
|
+
|
|
155
|
+
elif line.startswith("FRMC"):
|
|
156
|
+
match = re.match(r'^FRMC START:\s*(\d+)\s+FRMC END:\s*(\d+)'
|
|
157
|
+
r'\s+FRMC DURATION:\s*(\d+)', line, re.IGNORECASE)
|
|
158
|
+
|
|
159
|
+
if match is None:
|
|
160
|
+
return StmtCorruptRemark('FRMC', None, line_number)
|
|
161
|
+
|
|
162
|
+
else:
|
|
163
|
+
try:
|
|
164
|
+
return StmtFrmc(start=int(match.group(1)),
|
|
165
|
+
end=int(match.group(2)),
|
|
166
|
+
duration=int(match.group(3)),
|
|
167
|
+
line_number=line_number)
|
|
168
|
+
except ValueError as e:
|
|
169
|
+
return StmtCorruptRemark('FRMC', e, line_number)
|
|
170
|
+
|
|
142
171
|
else:
|
|
143
172
|
return StmtRemark(text=line, line_number=line_number)
|
|
144
173
|
|
|
@@ -148,27 +177,26 @@ def _parse_effects_name(line, line_number) -> StmtEffectsName:
|
|
|
148
177
|
return StmtEffectsName(name=name, line_number=line_number)
|
|
149
178
|
|
|
150
179
|
|
|
151
|
-
def _parse_split(line, line_number):
|
|
180
|
+
def _parse_split(line: str, line_number):
|
|
152
181
|
split_type = line[10:21]
|
|
153
|
-
is_video =
|
|
154
|
-
if split_type.startswith("VIDEO"):
|
|
155
|
-
is_video = True
|
|
182
|
+
is_video = split_type.startswith("VIDEO")
|
|
156
183
|
|
|
157
|
-
|
|
158
|
-
return StmtSplitEdit(video=is_video,
|
|
184
|
+
split_delay = line[24:35]
|
|
185
|
+
return StmtSplitEdit(video=is_video, delay=split_delay,
|
|
159
186
|
line_number=line_number)
|
|
160
187
|
|
|
161
188
|
|
|
162
|
-
def _parse_motion_memory(line, line_number):
|
|
163
|
-
|
|
164
|
-
|
|
189
|
+
# def _parse_motion_memory(line, line_number):
|
|
190
|
+
# return StmtMotionMemory(source="", fps="")
|
|
191
|
+
#
|
|
165
192
|
|
|
166
193
|
def _parse_unrecognized(line, line_number):
|
|
167
194
|
return StmtUnrecognized(content=line, line_number=line_number)
|
|
168
195
|
|
|
169
196
|
|
|
170
|
-
def _parse_columns_for_standard_form(line, event_field_length,
|
|
171
|
-
source_field_length,
|
|
197
|
+
def _parse_columns_for_standard_form(line: str, event_field_length: int,
|
|
198
|
+
source_field_length: int,
|
|
199
|
+
line_number: int):
|
|
172
200
|
col_widths = _edl_column_widths(event_field_length, source_field_length)
|
|
173
201
|
|
|
174
202
|
if sum(col_widths) > len(line):
|
|
@@ -185,9 +213,11 @@ def _parse_columns_for_standard_form(line, event_field_length,
|
|
|
185
213
|
source_out=column_strings[12].strip(),
|
|
186
214
|
record_in=column_strings[14].strip(),
|
|
187
215
|
record_out=column_strings[16].strip(),
|
|
188
|
-
line_number=line_number,
|
|
216
|
+
line_number=line_number,
|
|
217
|
+
source_field_size=source_field_length)
|
|
189
218
|
|
|
190
219
|
|
|
191
220
|
def _parse_source_umid_statement(line, line_number):
|
|
192
221
|
# trimmed = line[3:].strip()
|
|
193
|
-
return StmtSourceUMID(name=None, umid=None, line_number=line_number)
|
|
222
|
+
# return StmtSourceUMID(name=None, umid=None, line_number=line_number)
|
|
223
|
+
...
|
pycmx/statements.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# pycmx
|
|
2
|
+
# (c) 2025 Jamie Hardt
|
|
3
|
+
|
|
4
|
+
from typing import Any, NamedTuple
|
|
5
|
+
|
|
6
|
+
from .cdl import AscSopComponents
|
|
7
|
+
|
|
8
|
+
# type str = str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StmtTitle(NamedTuple):
|
|
12
|
+
title: str
|
|
13
|
+
line_number: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StmtFCM(NamedTuple):
|
|
17
|
+
drop: bool
|
|
18
|
+
line_number: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class StmtEvent(NamedTuple):
|
|
22
|
+
event: int
|
|
23
|
+
source: str
|
|
24
|
+
channels: str
|
|
25
|
+
trans: str
|
|
26
|
+
trans_op: str
|
|
27
|
+
source_in: str
|
|
28
|
+
source_out: str
|
|
29
|
+
record_in: str
|
|
30
|
+
record_out: str
|
|
31
|
+
source_field_size: int
|
|
32
|
+
line_number: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StmtAudioExt(NamedTuple):
|
|
36
|
+
audio3: bool
|
|
37
|
+
audio4: bool
|
|
38
|
+
line_number: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class StmtClipName(NamedTuple):
|
|
42
|
+
name: str
|
|
43
|
+
affect: str
|
|
44
|
+
line_number: int
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StmtSourceFile(NamedTuple):
|
|
48
|
+
filename: str
|
|
49
|
+
line_number: int
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class StmtCdlSop(NamedTuple):
|
|
53
|
+
line: str
|
|
54
|
+
cdl_sop: AscSopComponents[float]
|
|
55
|
+
line_number: int
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class StmtCdlSat(NamedTuple):
|
|
59
|
+
value: float
|
|
60
|
+
line_number: int
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class StmtFrmc(NamedTuple):
|
|
64
|
+
start: int
|
|
65
|
+
end: int
|
|
66
|
+
duration: int
|
|
67
|
+
line_number: int
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class StmtRemark(NamedTuple):
|
|
71
|
+
text: str
|
|
72
|
+
line_number: int
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class StmtEffectsName(NamedTuple):
|
|
76
|
+
name: str
|
|
77
|
+
line_number: int
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class StmtSourceUMID(NamedTuple):
|
|
81
|
+
name: str
|
|
82
|
+
umid: str
|
|
83
|
+
line_number: int
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class StmtSplitEdit(NamedTuple):
|
|
87
|
+
video: bool
|
|
88
|
+
delay: str
|
|
89
|
+
line_number: int
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class StmtUnrecognized(NamedTuple):
|
|
93
|
+
content: str
|
|
94
|
+
line_number: int
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class StmtCorruptRemark(NamedTuple):
|
|
98
|
+
selector: str
|
|
99
|
+
exception: Any
|
|
100
|
+
line_number: int
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# StmtMotionMemory = namedtuple(
|
|
104
|
+
# "MotionMemory", ["source", "fps"]) # FIXME needs more fields
|
pycmx/util.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pycmx
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Python CMX 3600 Edit Decision List Parser
|
|
5
|
-
Home-page: https://github.com/iluvcapra/pycmx
|
|
6
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
7
|
Keywords: parser,film,broadcast
|
|
8
8
|
Author: Jamie Hardt
|
|
9
9
|
Author-email: jamiehardt@me.com
|
|
@@ -17,6 +17,7 @@ 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: Programming Language :: Python :: 3.14
|
|
20
21
|
Classifier: Topic :: Multimedia
|
|
21
22
|
Classifier: Topic :: Multimedia :: Video
|
|
22
23
|
Classifier: Topic :: Text Processing
|
|
@@ -24,6 +25,7 @@ Provides-Extra: doc
|
|
|
24
25
|
Requires-Dist: sphinx (>=5.3.0) ; extra == "doc"
|
|
25
26
|
Requires-Dist: sphinx_rtd_theme (>=1.1.1) ; extra == "doc"
|
|
26
27
|
Project-URL: Documentation, https://pycmx.readthedocs.io/
|
|
28
|
+
Project-URL: Homepage, https://github.com/iluvcapra/pycmx
|
|
27
29
|
Project-URL: Repository, https://github.com/iluvcapra/pycmx.git
|
|
28
30
|
Project-URL: Tracker, https://github.com/iluvcapra/pycmx/issues
|
|
29
31
|
Description-Content-Type: text/markdown
|
|
@@ -35,17 +37,20 @@ Description-Content-Type: text/markdown
|
|
|
35
37
|
|
|
36
38
|
# pycmx
|
|
37
39
|
|
|
38
|
-
The `pycmx` package
|
|
39
|
-
its most most common variations.
|
|
40
|
+
The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
|
|
40
41
|
|
|
41
42
|
## Features
|
|
42
43
|
|
|
43
|
-
* The major variations of the CMX 3600: the standard, "File32", "File128" and
|
|
44
|
+
* The major variations of the CMX 3600: the standard, "File32", "File128" and
|
|
44
45
|
long Adobe Premiere event numbers are automatically detected and properly
|
|
45
|
-
read.
|
|
46
|
+
read. Event number field and source name field sizes are determined
|
|
47
|
+
dynamically for each statement for a high level of compliance at the expense
|
|
48
|
+
of strictness.
|
|
46
49
|
* Preserves relationship between events and individual edits/clips.
|
|
47
50
|
* Remark or comment fields with common recognized forms are read and
|
|
48
51
|
available to the client, including clip name and source file data.
|
|
52
|
+
* [ASC CDL][asc] and FRMC/VFX framecount statements are parsed and
|
|
53
|
+
decoded.
|
|
49
54
|
* Symbolically decodes transitions and audio channels.
|
|
50
55
|
* Does not parse or validate timecodes, does not enforce framerates, does not
|
|
51
56
|
parameterize timecode or framerates in any way. This makes the parser more
|
|
@@ -55,6 +60,8 @@ its most most common variations.
|
|
|
55
60
|
list and give the client the ability to extend the package with their own
|
|
56
61
|
parsing code.
|
|
57
62
|
|
|
63
|
+
[asc]: https://en.wikipedia.org/wiki/ASC_CDL
|
|
64
|
+
|
|
58
65
|
## Usage
|
|
59
66
|
|
|
60
67
|
### Opening and Parsing EDL Files
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
pycmx/__init__.py,sha256=u_VU3f3WltGoZzg_iZBLQZUtFqGFVZgBX3trqM7v3D4,365
|
|
2
|
+
pycmx/cdl.py,sha256=zONbFbvyETNs4u4mi7V-vePhO5VmQ_UE7wthbX0kigo,1041
|
|
3
|
+
pycmx/channel_map.py,sha256=_PIzTudMebzUfMWKNTA3i1egvmp4EsXqLPamCzjIUjk,3174
|
|
4
|
+
pycmx/edit.py,sha256=KmGbAd-wDbxFLmZKYgl0WW-qpYZbkyXeC5ViFiMvk8k,6446
|
|
5
|
+
pycmx/edit_list.py,sha256=sqmFkPG53k4-gQ9RMuSo2UjaR3K7o3akSM3sClTBWwc,3286
|
|
6
|
+
pycmx/event.py,sha256=cJ0XOiJ1upX_Wbo9Vo48FwVwSQWIQ0WSjn05Sp1mkDg,4524
|
|
7
|
+
pycmx/parse_cmx_events.py,sha256=TdM1Gjwi6PD3Bm0aFNDevPlSzZP2aP-uRsCjFmjDAwM,444
|
|
8
|
+
pycmx/parse_cmx_statements.py,sha256=wbheiju57CxfzYsYslykXzLMH-sP_WTMOzzmYZhpd18,7996
|
|
9
|
+
pycmx/statements.py,sha256=J6QRBQzIZ4q6so5-D3-t_2IsahMW6i3xCTPh7WqldgA,1598
|
|
10
|
+
pycmx/transition.py,sha256=IlVz2X3Z7CttJccqzzjhPPUNY2Pe8ZmVMIBnCrVLIG0,2364
|
|
11
|
+
pycmx/util.py,sha256=K1FVj2qptBtX-5RgBivc0RZl3JGdxAQ8x8WNhlBhY2Q,787
|
|
12
|
+
pycmx-1.4.0.dist-info/METADATA,sha256=YF0tm4MxDKn96CTFANOuglsgy_crhmWlHEG0pF7-4v4,4360
|
|
13
|
+
pycmx-1.4.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
14
|
+
pycmx-1.4.0.dist-info/licenses/LICENSE,sha256=JS087ZFloGaV7LHygeUaXmT-fGLx0VF30W62wDTLRCc,1056
|
|
15
|
+
pycmx-1.4.0.dist-info/RECORD,,
|
pycmx-1.3.0.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
pycmx/__init__.py,sha256=u_VU3f3WltGoZzg_iZBLQZUtFqGFVZgBX3trqM7v3D4,365
|
|
2
|
-
pycmx/channel_map.py,sha256=vkGonW2CY5F_TCbcJm9jFp6O7JsNUW4hew5q8KiVWLo,3169
|
|
3
|
-
pycmx/edit.py,sha256=6AfQAHh8f4aRFs211d2-Jmkv-a8CmoIduOFL3N2lrQg,3893
|
|
4
|
-
pycmx/edit_list.py,sha256=hjOsCxrGwQemjpmUGdfnJa0_rSYs-LSYkfOrjlqU-gw,3170
|
|
5
|
-
pycmx/event.py,sha256=6fwbCjo1mX8Zkmbd6Maw4-msWm2zYCUGRIGSeqfFWdw,3809
|
|
6
|
-
pycmx/parse_cmx_events.py,sha256=9d0jCvOXCt1RPSEBLZhLScSsGq3oDVa2bBDar8x4j5U,477
|
|
7
|
-
pycmx/parse_cmx_statements.py,sha256=Wi0wjlJsphR3WXWXcJn7fPRDKANitZpQwvOSZ84HY2A,7242
|
|
8
|
-
pycmx/transition.py,sha256=IlVz2X3Z7CttJccqzzjhPPUNY2Pe8ZmVMIBnCrVLIG0,2364
|
|
9
|
-
pycmx/util.py,sha256=z1QYA4qUxiaCeKPhCQEmgIyhr5MHxSknW2xjl6qs1kA,782
|
|
10
|
-
pycmx-1.3.0.dist-info/LICENSE,sha256=JS087ZFloGaV7LHygeUaXmT-fGLx0VF30W62wDTLRCc,1056
|
|
11
|
-
pycmx-1.3.0.dist-info/METADATA,sha256=drKnB4Am-o2JUh_dFsDItdFV5JQYarvgOfl88icsBKY,4027
|
|
12
|
-
pycmx-1.3.0.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
|
|
13
|
-
pycmx-1.3.0.dist-info/RECORD,,
|
|
File without changes
|