78 Commits

Author SHA1 Message Date
36be259177 documentation 2025-12-18 09:40:26 -08:00
4b73dc7730 removed breakpoints 2025-12-17 19:49:02 -08:00
3cf31fa462 autopep 2025-12-17 19:42:32 -08:00
33bd5a0001 autopep 2025-12-17 19:39:38 -08:00
ebdc73198c implementation of tolerant parsing 2025-12-17 19:38:53 -08:00
cf1b3fb42c Leaving this for now 2025-12-17 15:05:40 -08:00
6041d4158e Added problem edl 2025-12-17 14:21:51 -08:00
886561f2c5 test update 2025-12-16 19:37:24 -08:00
5e4bce7be8 documentation 2025-12-16 19:35:21 -08:00
7a2917c357 documentation 2025-12-16 19:34:21 -08:00
0dd5ab33a5 documentation 2025-12-16 19:31:57 -08:00
4471953696 documentation 2025-12-16 19:22:41 -08:00
45228dcdf4 documentation 2025-12-16 19:04:22 -08:00
09ccdb8c8d documentation 2025-12-16 19:04:02 -08:00
43dd48dea7 documentation 2025-12-16 18:59:40 -08:00
abfdfaffd8 documentation 2025-12-16 18:53:26 -08:00
48f41ef4dc documentation 2025-12-16 18:51:02 -08:00
139259777e documentation 2025-12-16 18:44:04 -08:00
f4f0ba9d74 documentation 2025-12-16 18:42:17 -08:00
a0c2bc77bf documentation 2025-12-16 18:29:08 -08:00
7d6a6e9a33 documentation 2025-12-16 18:27:00 -08:00
73d860aa49 documentation 2025-12-16 18:25:45 -08:00
Jamie Hardt
51e1946c5d Merge pull request #18 from iluvcapra/edit-class-type-annotations
Add type annotations to Edit class constructor and attributes for cla…
2025-12-16 18:16:55 -08:00
Jamie Hardt
16a61bfcb0 line lengths 2025-12-16 18:15:52 -08:00
Jamie Hardt
5a093e10d2 Add type annotations to Edit class constructor and attributes for clarity 2025-12-16 18:13:16 -08:00
58f28e3e2e documentation 2025-12-16 18:01:22 -08:00
5dc6da8f99 flakey 2025-12-16 17:49:14 -08:00
c9987dac91 docs 2025-12-16 17:48:21 -08:00
1d6c99aba3 docs 2025-12-16 17:45:37 -08:00
717b045967 docs 2025-12-16 17:44:45 -08:00
f0a445b2b2 docuentation 2025-12-16 17:43:11 -08:00
f968b777a8 Merge branch 'master' of https://github.com/iluvcapra/pycmx 2025-12-16 17:21:34 -08:00
89fdefa565 doc 2025-12-16 17:21:30 -08:00
Jamie Hardt
ead9f8aa13 Enhance docstring for unrecognized_statements
Added a newline for the sake of sphinx.
2025-12-16 17:13:33 -08:00
3248601445 autopep 2025-12-16 17:04:54 -08:00
82daa88b8d autopep 2025-12-16 17:03:10 -08:00
ed4b81adf3 corrupt remarks now in unrecognized statements 2025-12-16 17:01:44 -08:00
d17354682c autopep 2025-12-16 16:57:56 -08:00
f56a2aef4f tweaking generics for <3.10 support 2025-12-16 16:56:55 -08:00
7b875900f9 flakery 2025-12-16 16:54:45 -08:00
fa19b9841f flakery 2025-12-16 16:53:30 -08:00
437ebef9c6 flakery 2025-12-16 16:50:31 -08:00
fe0818bfcc Typing 2025-12-16 16:49:01 -08:00
69dee73299 Error handling during parsing 2025-12-16 16:38:38 -08:00
1d78f11b11 retyping some CDL items 2025-12-16 16:22:30 -08:00
d071d6c27e retyping some CDL items 2025-12-16 16:06:58 -08:00
785b7a9a08 retyping some CDL items 2025-12-16 16:00:04 -08:00
3cdae1761f retyping some CDL items 2025-12-16 15:57:34 -08:00
5c02d09a7a flake8 2025-12-16 14:06:09 -08:00
d0d30702ac flake8 2025-12-16 14:04:47 -08:00
cc07efef24 flake8 2025-12-16 14:03:02 -08:00
155011abfa flake8 2025-12-16 14:00:49 -08:00
e0d59e3ec7 flake8 2025-12-16 13:59:55 -08:00
8cdcccce45 flake8 2025-12-16 13:58:41 -08:00
656f546d12 copyright updates 2025-12-16 13:42:16 -08:00
14320c709c typing things 2025-12-16 13:25:13 -08:00
5ad938c54b typing things 2025-12-16 13:12:41 -08:00
ec8c3e30f5 typing things 2025-12-16 13:11:59 -08:00
30bbb05c2d typing things 2025-12-16 13:04:18 -08:00
dd78fcc1ac pylint 2025-12-16 13:01:53 -08:00
ecb09da7ba renamed type, typo 2025-12-16 12:53:37 -08:00
aed9560b1e Merge branch 'master' of https://github.com/iluvcapra/pycmx 2025-12-16 12:50:20 -08:00
27fcc2eb61 Updated readme 2025-12-16 12:49:11 -08:00
Jamie Hardt
5e0b5f4708 Merge pull request #17 from iluvcapra/16-feat-asc_cdl-and-frmc
CDL Statements: ASC_SOP, ASC_SAT and FRMC support
2025-12-16 12:39:41 -08:00
87dbbb1c68 autopep 2025-12-16 12:39:04 -08:00
229f6d646b FRMT implementation 2025-12-16 12:37:16 -08:00
23499b140c flake8 2025-12-16 12:21:51 -08:00
78f5c8ea08 Made imports explicit 2025-12-16 12:20:25 -08:00
878bdcc8c8 Made statement imports explicit 2025-12-16 12:18:50 -08:00
8a825c8164 ASC SOP and SAT support in Edit type 2025-12-16 12:14:26 -08:00
039bf8de6d autopep 2025-12-16 11:26:38 -08:00
73853d215e FRMC statement form implementation 2025-12-16 11:24:42 -08:00
2483e7fe43 Implemented Cdl ASC_SAT 2025-12-16 11:10:08 -08:00
6c7f7d2de1 Implemented SOP form parsing 2025-12-16 10:35:40 -08:00
2ee94ca358 Moved statement structs into a new file 2025-12-16 09:44:36 -08:00
f03b2e74bf Merge branch 'master' of https://github.com/iluvcapra/pycmx into 16-feat-asc_cdl-and-frmc 2025-12-16 09:42:29 -08:00
Jamie Hardt
52ecc02eae Merge pull request #15 from iluvcapra/14-issue-parsing-edl
Parsing odd EDLs re Issue #14
2025-12-16 09:37:09 -08:00
7a9e9627c3 Added EDL test cases 2025-12-16 09:32:32 -08:00
21 changed files with 1009 additions and 126 deletions

View File

@@ -5,17 +5,22 @@
# pycmx
The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and
its most most common variations.
The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
## Features
* The major variations of the CMX 3600: the standard, "File32", "File128" and
* The major variations of the CMX 3600: the standard, "File32", "File128" and
long Adobe Premiere event numbers are automatically detected and properly
read.
read. Event number field and source name field sizes are determined
dynamically for each statement for a high level of compliance at the expense
of strictness.
* An more relaxed "tolerant" mode allows parsing of an EDL file where columns
use non-standard widths.
* Preserves relationship between events and individual edits/clips.
* Remark or comment fields with common recognized forms are read and
available to the client, including clip name and source file data.
* [ASC CDL][asc] and FRMC/VFX framecount statements are parsed and
decoded.
* Symbolically decodes transitions and audio channels.
* Does not parse or validate timecodes, does not enforce framerates, does not
parameterize timecode or framerates in any way. This makes the parser more
@@ -25,6 +30,8 @@ its most most common variations.
list and give the client the ability to extend the package with their own
parsing code.
[asc]: https://en.wikipedia.org/wiki/ASC_CDL
## Usage
### Opening and Parsing EDL Files

View File

@@ -20,5 +20,6 @@ pycmx Classes
.. autoclass:: pycmx.channel_map.ChannelMap
:members:
.. automodule:: pycmx.cdl
:members:

View File

@@ -21,7 +21,7 @@ sys.path.insert(0, os.path.abspath('../..'))
# -- Project information -----------------------------------------------------
project = u'pycmx'
copyright = u'(c) 2025, Jamie Hardt'
copyright = u'(c) 2018-2025, Jamie Hardt'
author = u'Jamie Hardt'
release = importlib.metadata.version("pycmx")

View File

@@ -3,8 +3,75 @@
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to pycmx's documentation!
=================================
pycmx - A CMX EDL Parser in Python
====================================
Features
---------
The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
* The major variations of the CMX 3600: the standard, "File32", "File128" and
long Adobe Premiere event numbers are automatically detected and properly
read. Event number field and source name field sizes are determined
dynamically for each statement for a high level of compliance at the expense
of strictness.
* An more relaxed "tolerant" mode allows parsing of an EDL file where columns
use non-standard widths.
* Preserves relationship between events and individual edits/clips.
* Remark or comment fields with common recognized forms are read and
available to the client, including clip name and source file data.
* `ASC CDL`_ and FRMC statements are parsed and decoded.
* Symbolically decodes transitions and audio channels.
* Does not parse or validate timecodes, does not enforce framerates, does not
parameterize timecode or framerates in any way. This makes the parser more
tolerant of EDLs with mixed rates.
* Unrecognized lines are accessible on the `EditList` and `Event` classes
along with the line numbers, to help the client diagnose problems with a
list and give the client the ability to extend the package with their own
parsing code.
.. _ASC CDL: https://en.wikipedia.org/wiki/ASC_CDL
Getting Started
----------------
Install `pycmx` with pip, or add it with `uv` or your favorite tool.
.. code-block:: sh
pip install pycmx
`pycmx` parses an EDL with the :func:`~pycmx.parse_cmx_events.parse_cmx3600`
function:
.. code-block:: python
import pycmx
with open("tests/edls/TEST.edl") as f:
edl = pycmx.parse_cmx3600(f)
The `pycmx` parser reads each line from the input EDL and collects them into
`~pycmx.event.Event` objects. All individual edit actions that share the same
event number will be collected into a single Event, along with transitions and
any remark lines, including clip names, and CDL color commands.
.. code-block:: python
for event in edl.events:
print("- - - Event Info - - -")
print("Event No:", event.number)
for edit in event.edits:
print("On Line No:", edit.line_number)
print("Transition In:", edit.transition.kind)
print("Source Name:", edit.source)
print("Source In:", edit.source_in)
print("Source Out:", edit.source_out)
print("Rec In:", edit.record_in)
print("Rec Out:", edit.record_out)
print("ASC SOP:", edit.asc_sop)
.. toctree::
:maxdepth: 5

48
pycmx/cdl.py Normal file
View File

@@ -0,0 +1,48 @@
# pycmx
# (c) 2025 Jamie Hardt
from dataclasses import dataclass
from typing import Generic, NamedTuple, TypeVar
T = TypeVar('T')
@dataclass
class Rgb(Generic[T]):
"""
A tuple of three `T`s, where each is the respective red, green and blue
values of interest.
"""
red: T # : Red component
green: T # : Green component
blue: T # : Blue component
@dataclass
class AscSopComponents(Generic[T]):
"""
Fields in an ASC SOP (Slope-Offset-Power) color transfer function
statement.
The ASC SOP is a transfer function of the form:
:math:`y_{color} = (ax_{color} + b)^p`
for each color component the source, where the `slope` is `a`, `offset`
is `b` and `power` is `p`.
"""
slope: Rgb[T] # : The linear/slope component `a`
offset: Rgb[T] # : The constant/offset component `b`
power: Rgb[T] # : The exponential/power component `p`
class FramecountTriple(NamedTuple):
"""
Fields in an FRMC statement
"""
start: int
end: int
duration: int

View File

@@ -1,5 +1,5 @@
# pycmx
# (c) 2018 Jamie Hardt
# (c) 2018-2025 Jamie Hardt
from re import (compile, match)
from typing import Dict, Tuple, Generator

View File

@@ -1,9 +1,19 @@
# pycmx
# (c) 2018 Jamie Hardt
# (c) 2018-2025 Jamie Hardt
from pycmx.cdl import AscSopComponents, FramecountTriple
from pycmx.statements import (
StmtCdlSat,
StmtCdlSop,
StmtFrmc,
StmtEvent,
StmtAudioExt,
StmtClipName,
StmtSourceFile,
StmtEffectsName,
)
from .transition import Transition
from .channel_map import ChannelMap
# from .parse_cmx_statements import StmtEffectsName
from typing import Optional
@@ -14,14 +24,28 @@ class Edit:
recorder timecode in and out, a transition and channels.
"""
def __init__(self, edit_statement, audio_ext_statement,
clip_name_statement, source_file_statement,
trans_name_statement=None):
self.edit_statement = edit_statement
self.audio_ext = audio_ext_statement
self.clip_name_statement = clip_name_statement
self.source_file_statement = source_file_statement
self.trans_name_statement = trans_name_statement
def __init__(
self,
edit_statement: StmtEvent,
audio_ext_statement: Optional[StmtAudioExt],
clip_name_statement: Optional[StmtClipName],
source_file_statement: Optional[StmtSourceFile],
trans_name_statement: Optional[StmtEffectsName] = None,
asc_sop_statement: Optional[StmtCdlSop] = None,
asc_sat_statement: Optional[StmtCdlSat] = None,
frmc_statement: Optional[StmtFrmc] = None,
) -> None:
# Assigning types for the attributes explicitly
self._edit_statement: StmtEvent = edit_statement
self._audio_ext: Optional[StmtAudioExt] = audio_ext_statement
self._clip_name_statement: Optional[StmtClipName] = clip_name_statement
self._source_file_statement: Optional[StmtSourceFile] = \
source_file_statement
self._trans_name_statement: Optional[StmtEffectsName] = \
trans_name_statement
self._asc_sop_statement: Optional[StmtCdlSop] = asc_sop_statement
self._asc_sat_statement: Optional[StmtCdlSat] = asc_sat_statement
self._frmc_statement: Optional[StmtFrmc] = frmc_statement
@property
def line_number(self) -> int:
@@ -30,7 +54,7 @@ class Edit:
this edit. Line numbers a zero-indexed, such that the
"TITLE:" record is line zero.
"""
return self.edit_statement.line_number
return self._edit_statement.line_number
@property
def channels(self) -> ChannelMap:
@@ -38,30 +62,33 @@ class Edit:
Get the :obj:`ChannelMap` object associated with this Edit.
"""
cm = ChannelMap()
cm._append_event(self.edit_statement.channels)
if self.audio_ext is not None:
cm._append_ext(self.audio_ext)
cm._append_event(self._edit_statement.channels)
if self._audio_ext is not None:
cm._append_ext(self._audio_ext)
return cm
@property
def transition(self) -> Transition:
"""
Get the :obj:`Transition` object associated with this edit.
Get the :obj:`Transition` that initiates this edit.
"""
if self.trans_name_statement:
return Transition(self.edit_statement.trans,
self.edit_statement.trans_op,
self.trans_name_statement.name)
if self._trans_name_statement:
return Transition(
self._edit_statement.trans,
self._edit_statement.trans_op,
self._trans_name_statement.name,
)
else:
return Transition(self.edit_statement.trans,
self.edit_statement.trans_op, None)
return Transition(
self._edit_statement.trans, self._edit_statement.trans_op, None
)
@property
def source_in(self) -> str:
"""
Get the source in timecode.
"""
return self.edit_statement.source_in
return self._edit_statement.source_in
@property
def source_out(self) -> str:
@@ -69,7 +96,7 @@ class Edit:
Get the source out timecode.
"""
return self.edit_statement.source_out
return self._edit_statement.source_out
@property
def record_in(self) -> str:
@@ -77,7 +104,7 @@ class Edit:
Get the record in timecode.
"""
return self.edit_statement.record_in
return self._edit_statement.record_in
@property
def record_out(self) -> str:
@@ -85,7 +112,7 @@ class Edit:
Get the record out timecode.
"""
return self.edit_statement.record_out
return self._edit_statement.record_out
@property
def source(self) -> str:
@@ -93,19 +120,21 @@ class Edit:
Get the source column. This is the 8, 32 or 128-character string on the
event record line, this usually references the tape name of the source.
"""
return self.edit_statement.source
return self._edit_statement.source
@property
def black(self) -> bool:
"""
Black video or silence should be used as the source for this event.
The source field for thie edit was "BL". Black video or silence should
be used as the source for this event.
"""
return self.source == "BL"
@property
def aux_source(self) -> bool:
"""
An auxiliary source is the source of this event.
The source field for this edit was "AX". An auxiliary source is the
source for this event.
"""
return self.source == "AX"
@@ -115,10 +144,10 @@ class Edit:
Get the source file, as attested by a "* SOURCE FILE" remark on the
EDL. This will return None if the information is not present.
"""
if self.source_file_statement is None:
if self._source_file_statement is None:
return None
else:
return self.source_file_statement.filename
return self._source_file_statement.filename
@property
def clip_name(self) -> Optional[str]:
@@ -127,7 +156,49 @@ class Edit:
NAME" remark on the EDL. This will return None if the information is
not present.
"""
if self.clip_name_statement is None:
if self._clip_name_statement is None:
return None
else:
return self.clip_name_statement.name
return self._clip_name_statement.name
@property
def asc_sop(self) -> Optional[AscSopComponents[float]]:
"""
Get ASC CDL Slope-Offset-Power color transfer function for the edit,
if present. The ASC SOP is a transfer function of the form:
:math:`y = (ax + b)^p`
for each color component the source, where the `slope` is `a`, `offset`
is `b` and `power` is `p`.
"""
if self._asc_sop_statement is None:
return None
return self._asc_sop_statement.cdl_sop
@property
def asc_sat(self) -> Optional[float]:
"""
Get ASC CDL saturation value for clip, if present
"""
if self._asc_sat_statement is None:
return None
return self._asc_sat_statement.value
@property
def framecounts(self) -> Optional[FramecountTriple]:
"""
Get frame count offset data, if it exists. If an FRMC statement exists
in the EDL for the event it will give an integer frame count for the
edit's source in and out times.
"""
if not self._frmc_statement:
return None
return FramecountTriple(
start=self._frmc_statement.start,
end=self._frmc_statement.end,
duration=self._frmc_statement.duration,
)

View File

@@ -1,12 +1,12 @@
# pycmx
# (c) 2018 Jamie Hardt
# (c) 2018-2025 Jamie Hardt
from .parse_cmx_statements import (
StmtUnrecognized, StmtEvent, StmtSourceUMID)
from pycmx.statements import (StmtCorruptRemark, StmtTitle, StmtEvent,
StmtUnrecognized, StmtSourceUMID)
from .event import Event
from .channel_map import ChannelMap
from typing import Generator
from typing import Any, Generator
class EditList:
@@ -15,8 +15,8 @@ class EditList:
:func:`~pycmx.parse_cmx3600()`.
"""
def __init__(self, statements):
self.title_statement = statements[0]
def __init__(self, statements: list):
self.title_statement: StmtTitle = statements[0]
self.event_statements = statements[1:]
@property
@@ -31,11 +31,11 @@ class EditList:
(s for s in self.event_statements if type(s) is StmtEvent), None)
if first_event:
if first_event.format == 8:
if first_event.source_field_size == 8:
return '3600'
elif first_event.format == 32:
elif first_event.source_field_size == 32:
return 'File32'
elif first_event.format == 128:
elif first_event.source_field_size == 128:
return 'File128'
else:
return 'unknown'
@@ -63,13 +63,16 @@ class EditList:
return self.title_statement.title
@property
def unrecognized_statements(self) -> Generator[StmtUnrecognized,
None, None]:
def unrecognized_statements(self) -> Generator[Any, None, None]:
"""
A generator for all the unrecognized statements in the list.
A generator for all the unrecognized statements and
corrupt remarks in the list.
:yields: either a :class:`StmtUnrecognized` or
:class:`StmtCorruptRemark`
"""
for s in self.event_statements:
if type(s) is StmtUnrecognized:
if type(s) is StmtUnrecognized or type(s) in StmtCorruptRemark:
yield s
@property
@@ -90,8 +93,6 @@ class EditList:
else:
event_statements.append(stmt)
elif type(stmt) is StmtSourceUMID:
break
else:
event_statements.append(stmt)

View File

@@ -1,9 +1,10 @@
# pycmx
# (c) 2023 Jamie Hardt
# (c) 2023-2025 Jamie Hardt
from pycmx.statements import StmtFrmc
from .parse_cmx_statements import (
StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized,
StmtEffectsName)
StmtEffectsName, StmtCdlSop, StmtCdlSat)
from .edit import Edit
from typing import List, Generator, Optional, Tuple, Any
@@ -69,12 +70,14 @@ class Event:
the_zip.append(trans_names)
except IndexError:
the_zip.append([None] * len(edits_audio))
return [Edit(edit_statement=e1[0],
audio_ext_statement=e1[1],
clip_name_statement=n1,
source_file_statement=s1,
trans_name_statement=u1)
trans_name_statement=u1,
asc_sop_statement=self._asc_sop_statement(),
asc_sat_statement=self._asc_sat_statement(),
frmc_statement=self._frmc_statement())
for (e1, n1, s1, u1) in zip(*the_zip)]
@property
@@ -101,8 +104,24 @@ class Event:
def _statements_with_audio_ext(self) -> Generator[
Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
for (s1, s2) in zip(self.statements, self.statements[1:]):
if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
yield (s1, s2)
elif type(s1) is StmtEvent:
yield (s1, None)
if len(self.statements) == 1 and type(self.statements[0]) is StmtEvent:
yield (self.statements[0], None)
else:
for (s1, s2) in zip(self.statements, self.statements[1:]):
if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
yield (s1, s2)
elif type(s1) is StmtEvent:
yield (s1, None)
def _asc_sop_statement(self) -> Optional[StmtCdlSop]:
return next((s for s in self.statements if type(s) is StmtCdlSop),
None)
def _asc_sat_statement(self) -> Optional[StmtCdlSat]:
return next((s for s in self.statements if type(s) is StmtCdlSat),
None)
def _frmc_statement(self) -> Optional[StmtFrmc]:
return next((s for s in self.statements if type(s) is StmtFrmc), None)

View File

@@ -1,20 +1,20 @@
# pycmx
# (c) 2018 Jamie Hardt
# (c) 2018-2025 Jamie Hardt
# from collections import namedtuple
from typing import TextIO
from .parse_cmx_statements import (parse_cmx3600_statements)
from .edit_list import EditList
from typing import TextIO
def parse_cmx3600(f: TextIO) -> EditList:
def parse_cmx3600(f: TextIO, tolerant: bool = False) -> EditList:
"""
Parse a CMX 3600 EDL.
:param TextIO f: a file-like object, an opened CMX 3600 .EDL file.
:param bool tolerant: If `True`, a relaxed event line parsing method will
be used, in the case the default method fails.
:returns: An :class:`pycmx.edit_list.EditList`.
"""
statements = parse_cmx3600_statements(f)
statements = parse_cmx3600_statements(f, tolerant)
return EditList(statements)

View File

@@ -1,37 +1,25 @@
# pycmx
# (c) 2018 Jamie Hardt
# (c) 2018-2025 Jamie Hardt
import re
from collections import namedtuple
from typing import TextIO, List
from pycmx.cdl import AscSopComponents, Rgb
from .statements import (StmtCdlSat, StmtCdlSop, StmtCorruptRemark, StmtFrmc,
StmtRemark, StmtTitle, StmtUnrecognized, StmtFCM,
StmtAudioExt, StmtClipName, StmtEffectsName,
StmtEvent, StmtSourceFile, StmtSplitEdit)
from .util import collimate
StmtTitle = namedtuple("Title", ["title", "line_number"])
StmtFCM = namedtuple("FCM", ["drop", "line_number"])
StmtEvent = namedtuple("Event", ["event", "source", "channels", "trans",
"trans_op", "source_in", "source_out",
"record_in", "record_out", "format",
"line_number"])
StmtAudioExt = namedtuple("AudioExt", ["audio3", "audio4", "line_number"])
StmtClipName = namedtuple("ClipName", ["name", "affect", "line_number"])
StmtSourceFile = namedtuple("SourceFile", ["filename", "line_number"])
StmtRemark = namedtuple("Remark", ["text", "line_number"])
StmtEffectsName = namedtuple("EffectsName", ["name", "line_number"])
StmtSourceUMID = namedtuple("Source", ["name", "umid", "line_number"])
StmtSplitEdit = namedtuple("SplitEdit", ["video", "magnitude", "line_number"])
StmtMotionMemory = namedtuple(
"MotionMemory", ["source", "fps"]) # FIXME needs more fields
StmtUnrecognized = namedtuple("Unrecognized", ["content", "line_number"])
def parse_cmx3600_statements(file: TextIO) -> List[object]:
def parse_cmx3600_statements(file: TextIO,
tolerant: bool = False) -> List[object]:
"""
Return a list of every statement in the file argument.
"""
lines = file.readlines()
return [_parse_cmx3600_line(line.strip(), line_number)
return [_parse_cmx3600_line(line.strip(), line_number, tolerant)
for (line_number, line) in enumerate(lines)]
@@ -51,7 +39,8 @@ def _edl_column_widths(event_field_length, source_field_length) -> List[int]:
# 8,8,1,4,2,1,4,13,3,1,1]
def _parse_cmx3600_line(line: str, line_number: int) -> object:
def _parse_cmx3600_line(line: str, line_number: int,
tolerant: bool = False) -> object:
"""
Parses a single CMX EDL line.
@@ -63,27 +52,39 @@ def _parse_cmx3600_line(line: str, line_number: int) -> object:
if line.startswith("TITLE:"):
return _parse_title(line, line_number)
elif line.startswith("FCM:"):
if line.startswith("FCM:"):
return _parse_fcm(line, line_number)
elif line_matcher is not None:
if line_matcher is not None:
event_field_len = len(line_matcher.group(1))
source_field_len = len(line) - (event_field_len + 65)
return _parse_columns_for_standard_form(line, event_field_len,
source_field_len, line_number)
elif line.startswith("AUD"):
try:
return _parse_columns_for_standard_form(line, event_field_len,
source_field_len,
line_number)
except EventFormError:
if tolerant:
return _parse_columns_tolerant(line, line_number)
else:
return StmtUnrecognized(line, line_number)
if line.startswith("AUD"):
return _parse_extended_audio_channels(line, line_number)
elif line.startswith("*"):
if line.startswith("*"):
return _parse_remark(line[1:].strip(), line_number)
elif line.startswith(">>> SOURCE"):
if line.startswith(">>> SOURCE"):
return _parse_source_umid_statement(line, line_number)
elif line.startswith("EFFECTS NAME IS"):
if line.startswith("EFFECTS NAME IS"):
return _parse_effects_name(line, line_number)
elif line.startswith("SPLIT:"):
if line.startswith("SPLIT:"):
return _parse_split(line, line_number)
elif line.startswith("M2"):
return _parse_motion_memory(line, line_number)
else:
return _parse_unrecognized(line, line_number)
if line.startswith("M2"):
pass
# return _parse_motion_memory(line, line_number)
return _parse_unrecognized(line, line_number)
def _parse_title(line, line_num) -> StmtTitle:
@@ -95,14 +96,14 @@ def _parse_fcm(line, line_num) -> StmtFCM:
val = line[4:].strip()
if val == "DROP FRAME":
return StmtFCM(drop=True, line_number=line_num)
else:
return StmtFCM(drop=False, line_number=line_num)
return StmtFCM(drop=False, line_number=line_num)
def _parse_extended_audio_channels(line, line_number):
content = line.strip()
audio3 = True if "3" in content else False
audio4 = True if "4" in content else False
audio3 = "3" in content
audio4 = "4" in content
if audio3 or audio4:
return StmtAudioExt(audio3, audio4, line_number)
@@ -120,6 +121,62 @@ def _parse_remark(line, line_number) -> object:
elif line.startswith("SOURCE FILE:"):
return StmtSourceFile(filename=line[12:].strip(),
line_number=line_number)
elif line.startswith("ASC_SOP"):
group_patterns: list[str] = re.findall(r'\((.*?)\)', line)
v1: list[list[tuple[str, str]]] = \
[re.findall(r'(-?\d+(\.\d+)?)', a) for a in group_patterns]
v: list[list[str]] = [[a[0] for a in b] for b in v1]
if len(v) != 3 or any([len(a) != 3 for a in v]):
return StmtRemark(line, line_number)
else:
try:
return StmtCdlSop(cdl_sop=AscSopComponents(
slope=Rgb(red=float(v[0][0]), green=float(v[0][1]),
blue=float(v[0][2])),
offset=Rgb(red=float(v[1][0]), green=float(v[1][1]),
blue=float(v[1][2])),
power=Rgb(red=float(v[2][0]), green=float(v[2][1]),
blue=float(v[2][2]))
),
line_number=line_number)
except ValueError as e:
return StmtCorruptRemark('ASC_SOP', e, line_number)
elif line.startswith("ASC_SAT"):
value = re.findall(r'(-?\d+(\.\d+)?)', line)
if len(value) != 1:
return StmtRemark(line, line_number)
else:
try:
return StmtCdlSat(value=float(value[0][0]),
line_number=line_number)
except ValueError as e:
return StmtCorruptRemark('ASC_SAT', e, line_number)
elif line.startswith("FRMC"):
match = re.match(r'^FRMC START:\s*(\d+)\s+FRMC END:\s*(\d+)'
r'\s+FRMC DURATION:\s*(\d+)', line, re.IGNORECASE)
if match is None:
return StmtCorruptRemark('FRMC', None, line_number)
else:
try:
return StmtFrmc(start=int(match.group(1)),
end=int(match.group(2)),
duration=int(match.group(3)),
line_number=line_number)
except ValueError as e:
return StmtCorruptRemark('FRMC', e, line_number)
else:
return StmtRemark(text=line, line_number=line_number)
@@ -129,46 +186,78 @@ def _parse_effects_name(line, line_number) -> StmtEffectsName:
return StmtEffectsName(name=name, line_number=line_number)
def _parse_split(line, line_number):
def _parse_split(line: str, line_number):
split_type = line[10:21]
is_video = False
if split_type.startswith("VIDEO"):
is_video = True
is_video = split_type.startswith("VIDEO")
split_mag = line[24:35]
return StmtSplitEdit(video=is_video, magnitude=split_mag,
split_delay = line[24:35]
return StmtSplitEdit(video=is_video, delay=split_delay,
line_number=line_number)
def _parse_motion_memory(line, line_number):
return StmtMotionMemory(source="", fps="")
# def _parse_motion_memory(line, line_number):
# return StmtMotionMemory(source="", fps="")
#
class EventFormError(RuntimeError):
pass
def _parse_unrecognized(line, line_number):
return StmtUnrecognized(content=line, line_number=line_number)
def _parse_columns_for_standard_form(line, event_field_length,
source_field_length, line_number):
def _parse_columns_for_standard_form(line: str, event_field_length: int,
source_field_length: int,
line_number: int):
# breakpoint()
col_widths = _edl_column_widths(event_field_length, source_field_length)
if sum(col_widths) > len(line):
return StmtUnrecognized(content=line, line_number=line_number)
raise EventFormError()
column_strings = collimate(line, col_widths)
channels = column_strings[4].strip()
trans = column_strings[6].strip()
if len(channels) == 0 or len(trans) == 0:
raise EventFormError()
return StmtEvent(event=column_strings[0],
source=column_strings[2].strip(),
channels=column_strings[4].strip(),
trans=column_strings[6].strip(),
channels=channels,
trans=trans,
trans_op=column_strings[8].strip(),
source_in=column_strings[10].strip(),
source_out=column_strings[12].strip(),
record_in=column_strings[14].strip(),
record_out=column_strings[16].strip(),
line_number=line_number, format=source_field_length)
line_number=line_number,
source_field_size=source_field_length)
def _parse_columns_tolerant(line: str, line_number: int):
pattern = re.compile(r'^\s*(\d+)\s+(.{8,128}?)\s+'
r'(V|A|A2|AA|NONE|AA/V|A2/V|B)\s+'
r'(C|D|W|KB|K|KO)\s+(\d*)\s+(\d\d.\d\d.\d\d.\d\d)\s'
r'(\d\d.\d\d.\d\d.\d\d)\s(\d\d.\d\d.\d\d.\d\d)\s'
r'(\d\d.\d\d.\d\d.\d\d)'
)
match = pattern.match(line)
if match:
return StmtEvent(event=int(match.group(1)), source=match.group(2),
channels=match.group(3), trans=match.group(4),
trans_op=match.group(5), source_in=match.group(6),
source_out=match.group(7), record_in=match.group(8),
record_out=match.group(9), line_number=line_number,
source_field_size=len(match.group(2)))
else:
return StmtUnrecognized(line, line_number)
def _parse_source_umid_statement(line, line_number):
# trimmed = line[3:].strip()
return StmtSourceUMID(name=None, umid=None, line_number=line_number)
# return StmtSourceUMID(name=None, umid=None, line_number=line_number)
...

103
pycmx/statements.py Normal file
View File

@@ -0,0 +1,103 @@
# pycmx
# (c) 2025 Jamie Hardt
from typing import Any, NamedTuple
from pycmx.cdl import AscSopComponents
# type str = str
class StmtTitle(NamedTuple):
title: str
line_number: int
class StmtFCM(NamedTuple):
drop: bool
line_number: int
class StmtEvent(NamedTuple):
event: int
source: str
channels: str
trans: str
trans_op: str
source_in: str
source_out: str
record_in: str
record_out: str
source_field_size: int
line_number: int
class StmtAudioExt(NamedTuple):
audio3: bool
audio4: bool
line_number: int
class StmtClipName(NamedTuple):
name: str
affect: str
line_number: int
class StmtSourceFile(NamedTuple):
filename: str
line_number: int
class StmtCdlSop(NamedTuple):
cdl_sop: AscSopComponents[float]
line_number: int
class StmtCdlSat(NamedTuple):
value: float
line_number: int
class StmtFrmc(NamedTuple):
start: int
end: int
duration: int
line_number: int
class StmtRemark(NamedTuple):
text: str
line_number: int
class StmtEffectsName(NamedTuple):
name: str
line_number: int
class StmtSourceUMID(NamedTuple):
name: str
umid: str
line_number: int
class StmtSplitEdit(NamedTuple):
video: bool
delay: str
line_number: int
class StmtUnrecognized(NamedTuple):
content: str
line_number: int
class StmtCorruptRemark(NamedTuple):
selector: str
exception: Any
line_number: int
# StmtMotionMemory = namedtuple(
# "MotionMemory", ["source", "fps"]) # FIXME needs more fields

View File

@@ -1,5 +1,5 @@
# pycmx
# (c) 2018 Jamie Hardt
# (c) 2018-2025 Jamie Hardt
# Utility functions

View File

@@ -1 +1,2 @@
from . import test_parse
from . import test_issue_19

View File

@@ -0,0 +1,39 @@
TITLE: Final Master Generated by LTedlMixer...
0001 Z125C001_220217_ROLX V C 15:51:58:10 15:52:02:16 00:00:00:00 00:00:04:06
0002 B505C014_230224_RNBP V C 20:19:58:21 20:20:00:21 00:00:04:06 00:00:06:06
0003 B505C014_230224_RNBP V C 20:19:59:21 20:20:01:22 00:00:06:06 00:00:08:07
0004 B505C014_230224_RNBP V C 20:20:01:23 20:20:02:01 00:00:08:07 00:00:08:09
0005 B505C014_230224_RNBP V C 20:20:02:01 20:20:06:10 00:00:08:09 00:00:12:18
0006 B505C011_230224_RNBP V C 19:44:21:04 19:44:27:08 00:00:12:18 00:00:18:22
0007 B505C016_230224_RNBP V C 20:24:54:14 20:24:58:19 00:00:18:22 00:00:23:03
0008 Y022C029_211201_YNJI V C 12:42:37:04 12:42:39:14 00:00:23:03 00:00:25:13
0009 A054C025_211022_R24B V C 12:30:50:11 12:30:54:16 00:00:25:13 00:00:29:18
0010 Z040C026_211206_ROLX V C 14:42:25:21 14:42:28:17 00:00:29:18 00:00:32:14
0011 J001_C002_20211007_R V C 12:38:48:18 12:38:51:13 00:00:32:14 00:00:35:09
0012 C006C005_211007_RO2A V C 11:49:08:02 11:49:15:13 00:00:35:09 00:00:42:20
0013 A021C020_211007_R24B V C 18:14:52:00 18:14:58:22 00:00:42:20 00:00:49:18
0014 A023C013_211008_R24B V C 11:12:57:23 11:12:59:18 00:00:49:18 00:00:51:13
0015 U001C010_211029_R268 V C 04:38:42:21 04:38:49:21 00:00:51:13 00:00:58:13
0016 A021C009_211007_R24B V C 17:06:12:10 17:06:19:14 00:00:58:13 00:01:05:17
0017 A055C008_211022_R24B V C 15:16:03:10 15:16:05:09 00:01:05:17 00:01:07:16
0018 A055C008_211022_R24B V C 15:16:05:09 15:16:12:20 00:01:07:16 00:01:15:03
0019 A055C008_211022_R24B V C 15:16:12:20 15:16:14:19 00:01:15:03 00:01:17:02
0020 A056C011_211022_R24B V C 17:40:13:01 17:40:16:04 00:01:17:02 00:01:20:05
0021 A024C011_211008_R24B V C 17:32:07:02 17:32:10:01 00:01:20:05 00:01:23:04
0022 B070C001_211203_RP40 V C 17:39:20:20 17:39:22:14 00:01:23:04 00:01:24:22
0023 A055C019_211022_R24B V C 16:31:05:06 16:31:12:10 00:01:24:22 00:01:32:02
0024 A248C012_220224_R1Y2 V C 16:01:35:08 16:01:40:04 00:01:32:02 00:01:36:22
0025 A127C005_211206_R24B V C 10:58:23:06 10:58:24:09 00:01:36:22 00:01:38:01
0026 A040C006_211015_R24B V C 13:00:09:04 13:00:23:17 00:01:38:01 00:01:52:14
0027 A041C006_211015_R24B V C 16:10:32:08 16:10:35:12 00:01:52:14 00:01:55:18
0028 A040C006_211015_R24B V C 13:00:34:13 13:00:37:03 00:01:55:18 00:01:58:08
0029 A041C005_211015_R24B V C 15:57:22:05 15:57:27:11 00:01:58:08 00:02:03:14
0030 A040C008_211015_R24B V C 13:09:51:18 13:09:55:07 00:02:03:14 00:02:07:03
0031 A040C016_211015_R24B V C 14:09:15:11 14:09:20:02 00:02:07:03 00:02:11:18
0032 Z089C007_220122_ROLX V C 17:03:34:23 17:03:59:15 00:02:11:18 00:02:36:10
0033 A507C008_230227_RNHZ V C 09:55:35:10 09:55:41:02 00:02:36:10 00:02:42:02
0034 B049C021_211111_RP40 V C 17:38:55:11 17:38:56:17 00:02:42:02 00:02:43:08
0035 Z036C012_211202_ROLX V C 17:30:23:12 17:30:25:05 00:02:43:08 00:02:45:01
0036 A157C023_220112_R24B V C 14:13:18:04 14:13:20:06 00:02:45:01 00:02:47:03
0037 A095C014_211110_R24B V C 19:34:35:16 19:34:37:10 00:02:47:03 00:02:48:21
0038 Z089C010_220122_ROLX V C 17:28:55:21 17:28:58:09 00:02:48:21 00:02:51:09

View File

@@ -0,0 +1,226 @@
TITLE: cdl_example01
FCM: NON-DROP FRAME
000001 B_0031C010_241010_133208_h1C2T V C 13:30:27:13 13:30:29:01 02:13:08:12 02:13:10:00
*FROM CLIP NAME: 117C-3-B
*LOC: 02:13:08:12 Green Sc 117 INTERCUT
*ASC_SOP (0.9405 0.9439 0.9424)(-0.0500 -0.0276 -0.0144)(0.8888 0.9138 0.9896)
*ASC_SAT 0.9640
000002 B_0031C009_241010_132827_h1C2T V C 13:26:36:14 13:26:38:03 02:13:10:00 02:13:11:13
*VFX 117_GUD_0030 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 117C-2-B
*LOC: 02:13:10:04 White VFX 117_GUD_0030 add Norway landscape
*ASC_SOP (0.9282 0.9312 0.9296)(-0.0362 -0.0138 -0.0006)(0.8888 0.9138 0.9896)
*ASC_SAT 0.9640
000003 A_0038C002_241010_114016_h1EHP V C 12:41:35:05 12:41:41:11 02:13:12:13 02:13:18:19
*VFX 117_GUD_0060 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 117A-2-A
*LOC: 02:13:17:01 White VFX 117_GUD_0060 add Norway landscape
*ASC_SOP (0.8315 0.8307 0.8290)(0.0301 0.0416 0.0435)(0.9351 0.9413 0.9816)
*ASC_SAT 0.9640
000004 A_0038C016_241010_130056_h1EHP V C 14:02:14:03 14:02:15:03 02:13:18:19 02:13:19:19
*VFX 117_GUD_0070 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 117F-2
*LOC: 02:13:19:02 White VFX 117_GUD_0070 add Norway landscape
*ASC_SOP (0.9129 0.9045 0.8950)(0.0197 0.0394 0.0495)(0.9274 0.9472 0.9931)
*ASC_SAT 0.9640
000005 A_0038C006_241010_120437_h1EHP V C 13:06:10:07 13:06:11:08 02:13:19:19 02:13:20:20
*VFX 117_GUD_0080 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 117B-4-A
*LOC: 02:13:20:07 White VFX 117_GUD_0080 add Norway landscape
*ASC_SOP (0.8650 0.8604 0.8535)(0.0160 0.0319 0.0397)(0.9235 0.9485 0.9937)
*ASC_SAT 0.9640
000006 B_0031C005_241010_130536_h1C2T V C 13:03:45:20 13:03:47:14 02:13:20:20 02:13:22:14
*VFX 117_GUD_0090 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 117B-3-B
*LOC: 02:13:21:07 White VFX 117_GUD_0090 add Norway landscape
*ASC_SOP (0.7644 0.7755 0.7640)(0.0194 0.0392 0.0557)(0.8384 0.8687 0.9321)
*ASC_SAT 0.9640
000007 A_0038C016_241010_130056_h1EHP V C 14:02:21:15 14:02:24:12 02:13:22:14 02:13:25:11
*VFX 117_GUD_0100 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 117F-2
*LOC: 02:13:24:07 White VFX 117_GUD_0100 add Norway landscape
*ASC_SOP (0.9129 0.9045 0.8950)(0.0197 0.0394 0.0495)(0.9274 0.9472 0.9931)
*ASC_SAT 0.9640
000008 A_0038C006_241010_120437_h1EHP V C 13:06:16:18 13:06:23:09 02:13:25:11 02:13:32:02
*VFX 117_GUD_0110 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 117B-4-A
*LOC: 02:13:28:06 White VFX 117_GUD_0110 add Norway landscape
*ASC_SOP (0.8650 0.8604 0.8535)(0.0160 0.0319 0.0397)(0.9235 0.9485 0.9937)
*ASC_SAT 0.9640
000009 A_0038C011_241010_123111_h1EHP V C 13:32:49:04 13:32:51:01 02:13:32:02 02:13:33:23
*FROM CLIP NAME: 117C-4
*ASC_SOP (0.8656 0.8610 0.8542)(0.0153 0.0312 0.0389)(0.9318 0.9425 0.9884)
*ASC_SAT 0.9640
000010 A_0038C006_241010_120437_h1EHP V C 13:06:25:06 13:06:27:14 02:13:33:23 02:13:36:07
*VFX 117_GUD_0120 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 117B-4-A
*LOC: 02:13:35:01 White VFX 117_GUD_0120 add Norway landscape
*ASC_SOP (0.8650 0.8604 0.8535)(0.0160 0.0319 0.0397)(0.9235 0.9485 0.9937)
*ASC_SAT 0.9640
000011 A_0038C011_241010_123111_h1EHP V C 13:32:53:16 13:32:54:23 02:13:36:07 02:13:37:14
*FROM CLIP NAME: 117C-4
*ASC_SOP (0.8656 0.8610 0.8542)(0.0153 0.0312 0.0389)(0.9318 0.9425 0.9884)
*ASC_SAT 0.9640
000012 A_0038C006_241010_120437_h1EHP V C 13:06:30:14 13:06:34:01 02:13:37:14 02:13:41:01
*VFX 117_GUD_0130 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 117B-4-A
*LOC: 02:13:39:05 White VFX 117_GUD_0130 add Norway landscape
*ASC_SOP (0.8650 0.8604 0.8535)(0.0160 0.0319 0.0397)(0.9235 0.9485 0.9937)
*ASC_SAT 0.9640
000013 A_0038C012_241010_124231_h1EHP V C 13:44:10:05 13:44:12:06 02:13:41:01 02:13:43:02
*VFX 117_GUD_0140 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 117D-1
*LOC: 02:13:42:03 White VFX 117_GUD_0140 add Norway landscape
*ASC_SOP (0.8791 0.8754 0.8660)(-0.0000 0.0150 0.0256)(0.9310 0.9432 0.9893)
*ASC_SAT 0.9640
000014 A_0038C014_241010_125128_h1EHP V C 13:52:56:16 13:53:01:13 02:13:43:02 02:13:47:23
*VFX 117_GUD_0150 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 117E-2 PU
*LOC: 02:13:45:10 White VFX 117_GUD_0150 add Norway landscape
*ASC_SOP (0.9320 0.9203 0.9093)(-0.0262 -0.0022 0.0097)(0.9196 0.9569 1.0007)
*ASC_SAT 0.9640
000015 D006C0011_241001_Q3J911 V C 14:26:00:22 14:26:03:23 02:13:47:23 02:13:51:00
*FROM CLIP NAME: 121B-4*
*LOC: 02:13:47:23 Green Sc 121
*ASC_SOP (0.9688 0.9286 0.9044)(-0.0050 0.0335 0.0584)(1.0019 1.0499 1.0817)
*ASC_SAT 0.8500
000016 D006C0013_241001_Q3J911 V C 14:38:25:03 14:38:27:00 02:13:51:00 02:13:52:21
*FROM CLIP NAME: 121C-2
*ASC_SOP (0.9700 0.9296 0.9054)(-0.0053 0.0332 0.0581)(0.9871 1.0506 1.0894)
*ASC_SAT 0.8500
000017 D006C0011_241001_Q3J911 V C 14:26:10:22 14:26:13:15 02:13:52:21 02:13:55:14
*FROM CLIP NAME: 121B-4*
*ASC_SOP (0.9688 0.9286 0.9044)(-0.0050 0.0335 0.0584)(1.0019 1.0499 1.0817)
*ASC_SAT 0.8500
000018 A_0038C027_241010_145134_h1EHP V C 15:54:02:03 15:54:13:04 02:13:55:14 02:14:06:15
*VFX 117_GUD_0160 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 120C-3
*LOC: 02:13:55:14 Green Sc 120 INTERCUT
*LOC: 02:13:56:13 White VFX 117_GUD_0160 add Norway landscape
*ASC_SOP (0.9812 0.9783 1.0016)(-0.0737 -0.0335 -0.0141)(0.8337 0.8918 1.0014)
*ASC_SAT 0.9640
000019 A_0044C012_241015_112339_h1EHP V C 12:24:45:15 12:24:55:14 02:14:07:15 02:14:17:14
*VFX 124_SYF_0010 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124-3-A
*LOC: 02:14:07:15 Green Sc 124
*LOC: 02:14:12:04 White VFX 124_SYF_0010 add Norway landscape
*ASC_SOP (0.8971 0.8843 0.8818)(0.0070 0.0211 0.0238)(0.9792 0.9850 1.0157)
*ASC_SAT 1.0000
000020 A_0044C020_241015_115202_h1EHP V C 12:53:11:04 12:53:12:19 02:14:17:14 02:14:19:05
*FROM CLIP NAME: 124B-5
*ASC_SOP (0.9201 0.9118 0.9150)(-0.0137 0.0007 0.0036)(0.9530 0.9664 1.0033)
*ASC_SAT 1.0000
000021 A_0044C013_241015_113513_h1EHP V C 12:36:33:02 12:36:36:13 02:14:19:05 02:14:22:16
*VFX 124_SYF_0020 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124A-1-A
*LOC: 02:14:20:23 White VFX 124_SYF_0020 add Norway landscape
*ASC_SOP (0.8897 0.8752 0.8668)(0.0033 0.0195 0.0290)(0.9671 0.9811 1.0153)
*ASC_SAT 1.0000
000022 A_0044C020_241015_115202_h1EHP V C 12:53:18:02 12:53:20:19 02:14:22:16 02:14:25:09
*FROM CLIP NAME: 124B-5
*ASC_SOP (0.9201 0.9118 0.9150)(-0.0137 0.0007 0.0036)(0.9530 0.9664 1.0033)
*ASC_SAT 1.0000
000023 A_0044C013_241015_113513_h1EHP V C 12:36:36:22 12:36:38:07 02:14:25:09 02:14:26:18
*VFX 124_SYF_0025 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124A-1-A
*LOC: 02:14:25:23 White VFX 124_SYF_0025 add Norway landscape
*ASC_SOP (0.8897 0.8752 0.8668)(0.0033 0.0195 0.0290)(0.9671 0.9811 1.0153)
*ASC_SAT 1.0000
000024 A_0044C012_241015_112339_h1EHP V C 12:25:01:21 12:25:03:09 02:14:26:18 02:14:28:06
*VFX 124_SYF_0030 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124-3-A
*LOC: 02:14:27:09 White VFX 124_SYF_0030 add Norway landscape
*ASC_SOP (0.8971 0.8843 0.8818)(0.0070 0.0211 0.0238)(0.9792 0.9850 1.0157)
*ASC_SAT 1.0000
000025 A_0044C013_241015_113513_h1EHP V C 12:36:39:11 12:36:41:13 02:14:28:06 02:14:30:08
*VFX 124_SYF_0040 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124A-1-A
*LOC: 02:14:28:22 White VFX 124_SYF_0040 add Norway landscape
*ASC_SOP (0.8897 0.8752 0.8668)(0.0033 0.0195 0.0290)(0.9671 0.9811 1.0153)
*ASC_SAT 1.0000
000026 A_0044C020_241015_115202_h1EHP V C 12:53:23:11 12:53:25:13 02:14:30:08 02:14:32:10
*VFX 124_SYF_0043 ADD NORWAY BG
*FROM CLIP NAME: 124B-5
*LOC: 02:14:31:13 White VFX 124_SYF_0043 add Norway bg
*ASC_SOP (0.9201 0.9118 0.9150)(-0.0137 0.0007 0.0036)(0.9530 0.9664 1.0033)
*ASC_SAT 1.0000
000027 A_0044C013_241015_113513_h1EHP V C 12:36:41:18 12:36:43:05 02:14:32:10 02:14:33:21
*VFX 124_SYF_0048 ADD NORWAY BG
*FROM CLIP NAME: 124A-1-A
*LOC: 02:14:33:11 White VFX 124_SYF_0048 add Norway bg
*ASC_SOP (0.8897 0.8752 0.8668)(0.0033 0.0195 0.0290)(0.9671 0.9811 1.0153)
*ASC_SAT 1.0000
000028 A_0044C019_241015_115048_h1EHP V C 12:52:16:18 12:52:18:13 02:14:33:21 02:14:35:16
*VFX 124_SYF_0050 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124B-4
*LOC: 02:14:34:16 White VFX 124_SYF_0050 add Norway landscape
*ASC_SOP (0.9162 0.9080 0.9112)(-0.0095 0.0049 0.0078)(0.9530 0.9664 1.0033)
*ASC_SAT 1.0000
000029 A_0044C014_241015_113829_h1EHP V C 12:39:54:17 12:39:57:03 02:14:35:16 02:14:38:02
*VFX 124_SYF_0060 ADD NORWAY BG
*FROM CLIP NAME: 124A-2-A
*LOC: 02:14:36:16 White VFX 124_SYF_0060 Add Norway bg
*ASC_SOP (0.8898 0.8754 0.8669)(0.0032 0.0194 0.0288)(0.9671 0.9811 1.0153)
*ASC_SAT 1.0000
000030 B_0037C009_241015_122652_h1C2T V C 12:25:10:01 12:25:11:05 02:14:38:02 02:14:39:06
*VFX 124_SYF_0073 ADD NORWAY BG
*FROM CLIP NAME: 124-3-B
*LOC: 02:14:38:02 Green Sc 125
*LOC: 02:14:38:22 White VFX 124_SYF_0073 Add Norway bg
*ASC_SOP (0.9201 0.9118 0.9150)(-0.0137 0.0007 0.0036)(0.9530 0.9664 1.0033)
*ASC_SAT 1.0000
000031 A_0045C002_241015_131216_h1EHP V C 14:13:16:23 14:13:18:23 02:14:39:06 02:14:41:06
*VFX 124_SYF_0078 ADD NORWAY BG
*FROM CLIP NAME: 124E-2
*LOC: 02:14:40:01 White VFX 124_SYF_0078 Add Norway bg
*ASC_SOP (0.9201 0.9118 0.9150)(-0.0137 0.0007 0.0036)(0.9530 0.9664 1.0033)
*ASC_SAT 1.0000
000032 B_0037C009_241015_122652_h1C2T V C 12:25:11:13 12:25:12:06 02:14:41:06 02:14:41:23
*VFX 124_SYF_0080 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124-3-B
*LOC: 02:14:41:09 White VFX 124_SYF_0080 add Norway landscape
*ASC_SOP (0.9201 0.9118 0.9150)(-0.0137 0.0007 0.0036)(0.9530 0.9664 1.0033)
*ASC_SAT 1.0000
000033 D007C0005_241008_Q3J912 V C 13:19:00:22 13:19:02:15 02:14:41:23 02:14:43:16
*VFX 124_SYF_0100 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124C-4
*LOC: 02:14:42:08 White VFX 124_SYF_0100 add Norway landscape
*ASC_SOP (0.8789 0.8685 0.8723)(0.0094 0.0316 0.0328)(0.9300 0.9406 1.0012)
*ASC_SAT 1.0000
000034 A_0045C002_241015_131216_h1EHP V C 14:13:20:03 14:13:20:16 02:14:43:16 02:14:44:05
*VFX 124_SYF_0090 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124E-2
*LOC: 02:14:43:21 White VFX 124_SYF_0090 add Norway landscape
*ASC_SOP (0.9201 0.9118 0.9150)(-0.0137 0.0007 0.0036)(0.9530 0.9664 1.0033)
*ASC_SAT 1.0000
000035 D007C0007_241008_Q3J912 V C 13:26:59:07 13:27:00:18 02:14:44:05 02:14:45:16
*VFX 124_SYF_0110 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124D-2
*LOC: 02:14:44:19 White VFX 124_SYF_0110 add Norway landscape
*ASC_SOP (0.9058 0.8914 0.8960)(-0.0006 0.0207 0.0219)(0.9087 0.9191 0.9783)
*ASC_SAT 1.0000
000036 D007C0005_241008_Q3J912 V C 13:19:04:09 13:19:07:01 02:14:45:16 02:14:48:08
*VFX 124_SYF_0120 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124C-4
*LOC: 02:14:47:22 White VFX 124_SYF_0120 add Norway landscape
*ASC_SOP (0.8789 0.8685 0.8723)(0.0094 0.0316 0.0328)(0.9300 0.9406 1.0012)
*ASC_SAT 1.0000
000037 D007C0007_241008_Q3J912 V C 13:27:03:08 13:27:04:10 02:14:48:08 02:14:49:10
*VFX 124_SYF_0130 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124D-2
*LOC: 02:14:48:19 White VFX 124_SYF_0130 add Norway landscape
*ASC_SOP (0.9058 0.8914 0.8960)(-0.0006 0.0207 0.0219)(0.9087 0.9191 0.9783)
*ASC_SAT 1.0000
000038 D007C0003_241008_Q3J912 V C 13:15:29:20 13:15:30:23 02:14:49:10 02:14:50:13
*VFX 124_SYF_0140 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 124C-2
*LOC: 02:14:49:15 White VFX 124_SYF_0140 add Norway landscape
*ASC_SOP (0.8711 0.8610 0.8647)(0.0125 0.0345 0.0357)(0.9300 0.9406 1.0012)
*ASC_SAT 1.0000
000039 A_0045C009_241015_135209_h1EHP V C 14:53:13:14 14:53:15:09 02:14:50:13 02:14:52:08
*VFX 124_SYF_0150 ADD NORWAY LANDSCAPE
*FROM CLIP NAME: 125A-3*
*LOC: 02:14:50:13 Green Sc 126
*LOC: 02:14:50:14 White VFX 124_SYF_0150 add Norway landscape
*ASC_SOP (0.9541 0.9282 0.9124)(-0.0197 0.0032 0.0111)(0.9317 0.9350 0.9640)
*ASC_SAT 1.0000

View File

@@ -0,0 +1,17 @@
TITLE: cdl_example02
FCM: NON-DROP FRAME
000001 A205C016_220204_R24B V C 22:47:18:20 22:47:21:16 03:09:09:03 03:09:11:23
*FROM CLIP NAME: 49D-3
*ASC_SOP (0.98875 0.9878 0.98659)(-0.0008 0.00263 -0.00269)(0.9769 0.9767 0.97709)
*ASC_SAT 1.0
*SOURCE FILE: A205C016_220204_R24B
000002 A238C007_220221_R24B V C 11:53:01:13 11:53:05:20 03:09:55:18 03:10:00:01
*FROM CLIP NAME: 52B-7
*ASC_SOP (1.05572 1.06914 1.05607)(-0.03004 -0.03044 -0.03044)(1.02112 1.01956 1.01707)
*ASC_SAT 1.0
*SOURCE FILE: A238C007_220221_R24B
000004 A239C004_220221_R24B V C 15:19:53:22 15:19:55:02 03:10:00:01 03:10:01:05
*FROM CLIP NAME: 52G-4*
*ASC_SOP (1.00515 0.99542 0.9934)(-0.02412 -0.01467 -0.01351)(0.97348 0.97074 0.96887)
*ASC_SAT 1.0
*SOURCE FILE: A239C004_220221_R24B

View File

@@ -0,0 +1,7 @@
TITLE: cdl_frmc_example01
FCM: NON-DROP FRAME
000001 C004C008_240813ZW V C 14:41:59:15 14:42:03:21 01:00:00:00 01:00:04:06
*FROM CLIP NAME: QTLF_101_010_113_BG_01_V001
*FRMC START: 1001 FRMC END: 1102 FRMC DURATION:102
*ASC_SOP (1.04751 1.0378 1.02485)(-0.00909 0.00062 0.01468)(1.0 1.0 1.0)
*ASC_SAT 1.0

View File

@@ -0,0 +1,123 @@
TITLE: cdl_frmc_example02
FCM: NON-DROP FRAME
000001 A_0095C008_250116_182427_h1DQ9 V C 17:31:54:08 17:31:58:00 01:00:00:00 01:00:03:16
*FROM CLIP NAME: 000_trl_0010_bg01_v01
*FRMC start: 1001 FRMC end: 1088 FRMC duration:88
*ASC_SOP (1.0035 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000002 V204_01051811_C010 V C 13:07:43:14 13:07:49:06 01:00:03:16 01:00:09:08
*FROM CLIP NAME: 000_trl_0010_el01_v01
*FRMC start: 1001 FRMC end: 1136 FRMC duration:136
*ASC_SOP (1.0 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000003 A_0092C017_250115_182121_h1DQ9 V C 17:27:07:01 17:27:12:09 01:00:09:08 01:00:14:16
*FROM CLIP NAME: 027_mtr_1770_bg01_v01
*FRMC start: 1001 FRMC end: 1128 FRMC duration:128
*ASC_SOP (1.0 1.0 1.0)(0.0 -0.004 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000004 A_0092C018_250115_182308_h1DQ9 V C 17:28:39:03 17:28:51:00 01:00:14:16 01:00:26:13
*FROM CLIP NAME: 027_mtr_1770_rf01_v01
*FRMC start: 1001 FRMC end: 1285 FRMC duration:285
*ASC_SOP (1.0 1.0 1.0)(0.0 -0.004 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000005 C_0122C010_250219_123205_h1E5N V C 11:34:58:01 11:35:01:07 01:00:26:13 01:00:29:19
*FROM CLIP NAME: 084_ebk_2120_bg01_v01
*FRMC start: 1001 FRMC end: 1078 FRMC duration:78
*ASC_SOP (1.0 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000006 C_0122C011_250219_123354_h1E5N V C 11:36:19:22 11:36:40:04 01:00:29:19 01:00:50:01
*FROM CLIP NAME: 084_ebk_2120_rf01_v01
*FRMC start: 1001 FRMC end: 1486 FRMC duration:486
*ASC_SOP (1.0 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000007 B_0124C002_250219_045355_h1CUT V C 12:00:32:21 12:00:36:01 01:00:50:01 01:00:53:05
*FROM CLIP NAME: 084_ebk_2140_bg01_v01
*FRMC start: 1001 FRMC end: 1076 FRMC duration:76
*ASC_SOP (1.0 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000008 C_0126C013_250221_121324_h1E5N V C 11:16:03:18 11:16:09:11 01:00:53:05 01:00:58:22
*FROM CLIP NAME: 084_ebk_2280_bg01_v01
*FRMC start: 1001 FRMC end: 1137 FRMC duration:137
*ASC_SOP (1.0 1.0 0.999)(-0.0078 -0.0078 -0.0098)(1.0 1.0 1.0)
*ASC_SAT 1.0
000009 D_0079C004_250223_110040_h1DZT V C 11:04:05:08 11:04:08:08 01:00:58:22 01:01:01:22
*FROM CLIP NAME: 086_ebk_4160_bg01_v01
*FRMC start: 1001 FRMC end: 1072 FRMC duration:72
*ASC_SOP (1.0031 1.003 1.0025)(-0.0116 -0.0191 -0.0266)(1.0 1.0 1.0)
*ASC_SAT 1.0
000010 D_0079C005_250223_110303_h1DZT V C 11:06:01:07 11:06:14:12 01:01:01:22 01:01:15:03
*FROM CLIP NAME: 086_ebk_4160_rf01_v01
*FRMC start: 1001 FRMC end: 1317 FRMC duration:317
*ASC_SOP (1.0 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000011 W_0005C003_250223_105431_h1EPT V C 09:59:09:05 09:59:11:19 01:01:15:03 01:01:17:17
*FROM CLIP NAME: 086_ebk_4180_bg01_v01
*FRMC start: 1001 FRMC end: 1062 FRMC duration:62
*ASC_SOP (1.0198 1.0198 1.0193)(-0.0019 -0.0082 -0.0174)(1.0 1.0 1.0)
*ASC_SAT 1.0
000012 W_0005C004_250223_110023_h1EPT V C 10:03:33:16 10:03:41:05 01:01:17:17 01:01:25:06
*FROM CLIP NAME: 086_ebk_4180_rf01_v01
*FRMC start: 1001 FRMC end: 1181 FRMC duration:181
*ASC_SOP (1.0 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000013 D_0079C001_250223_095442_h1DZT V C 09:59:09:13 09:59:12:23 01:01:25:06 01:01:28:16
*FROM CLIP NAME: 086_ebk_4200_bg01_v01
*FRMC start: 1001 FRMC end: 1082 FRMC duration:82
*ASC_SOP (1.0031 1.003 1.0025)(0.0067 -0.0008 -0.0083)(1.0 1.0 1.0)
*ASC_SAT 1.0
000014 D_0079C002_250223_100041_h1DZT V C 10:03:42:19 10:03:51:04 01:01:28:16 01:01:37:01
*FROM CLIP NAME: 086_ebk_4200_rf01_v01
*FRMC start: 1001 FRMC end: 1201 FRMC duration:201
*ASC_SOP (1.0 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000015 A_0095C006_250116_181426_h1DQ9 V C 17:20:11:09 17:21:44:21 01:01:37:01 01:03:10:13
*FROM CLIP NAME: 088_mcm_0200_bg01_v01
*FRMC start: 1001 FRMC end: 3244 FRMC duration:2244
*ASC_SOP (1.0035 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000016 V204_01051756_C008 V C 12:54:36:09 12:54:43:14 01:03:10:13 01:03:17:18
*FROM CLIP NAME: 088_mcm_0200_el02_v01
*FRMC start: 1829 FRMC end: 2001 FRMC duration:173
*ASC_SOP (1.0 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000017 V204_01051808_C009 V C 13:03:57:17 13:04:08:00 01:03:17:18 01:03:28:01
*FROM CLIP NAME: 088_mcm_0200_el03_v01
*FRMC start: 2592 FRMC end: 2838 FRMC duration:247
*ASC_SOP (1.0 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000018 V204_01051811_C010 V C 13:07:57:07 13:08:04:12 01:03:28:01 01:03:35:06
*FROM CLIP NAME: 088_mcm_0200_el04_v01
*FRMC start: 3072 FRMC end: 3244 FRMC duration:173
*ASC_SOP (1.0 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000019 A_0095C012_250116_184034_h1DQ9 V C 17:46:03:10 17:46:11:09 01:03:35:06 01:03:43:05
*FROM CLIP NAME: 088_mcm_0200_rf01_v01
*FRMC start: 1001 FRMC end: 1191 FRMC duration:191
*ASC_SOP (1.0035 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000020 A_0095C013_250116_184129_h1DQ9 V C 17:47:00:10 17:47:21:07 01:03:43:05 01:04:04:02
*FROM CLIP NAME: 088_mcm_0200_rf02_v01
*FRMC start: 1001 FRMC end: 1501 FRMC duration:501
*ASC_SOP (1.0035 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000021 A_0095C006_250116_181426_h1DQ9 V C 17:21:43:22 17:21:58:07 01:04:04:02 01:04:18:11
*FROM CLIP NAME: 088_mcm_0240_bg01_V01
*FRMC start: 1001 FRMC end: 1345 FRMC duration:345
*ASC_SOP (1.0035 1.0 1.0)(0.0 0.0 0.0)(1.0 1.0 1.0)
*ASC_SAT 1.0
000022 A_0002C019_241030_174120_h1DQ9 V C 16:43:24:02 16:43:27:09 01:04:18:11 01:04:21:18
*FROM CLIP NAME: fv025_prw_0160_bg01_v01
*FRMC start: 1001 FRMC end: 1079 FRMC duration:79
000023 018_hdh_0040_lineup_v0003 V C 00:00:42:09 00:00:48:16 01:04:21:18 01:04:28:01
*FROM CLIP NAME: fv025_prw_0160_tv01_v01
*FRMC start: 1017 FRMC end: 1167 FRMC duration:151
000024 Z_0003C015_241120_031008_h1CIM V C 14:09:11:18 14:09:14:12 01:04:28:01 01:04:30:19
*FROM CLIP NAME: fv075_lgk_0500_bg01_v01
*FRMC start: 1001 FRMC end: 1066 FRMC duration:66
*ASC_SOP (1.005 0.9993 0.9917)(-0.005 0.0007 0.0083)(1.0 1.0 1.0)
*ASC_SAT 1.0
000025 Z_0003C011_241120_022000_h1CIM V C 13:19:03:19 13:19:16:04 01:04:30:19 01:04:43:04
*FROM CLIP NAME: fv075_lgk_0500_rf01_v01
*FRMC start: 1001 FRMC end: 1297 FRMC duration:297
*ASC_SOP (1.005 0.9993 0.9917)(-0.005 0.0007 0.0083)(1.0 1.0 1.0)
*ASC_SAT 1.0

25
tests/test_issue_19.py Normal file
View File

@@ -0,0 +1,25 @@
from unittest import TestCase
from pycmx import parse_cmx3600
class Issue19Test(TestCase):
def setUp(self):
self.f = open("tests/edls/ISSUE_19_unusual01.edl")
def test_parse(self):
edl = parse_cmx3600(self.f, tolerant=True)
for event in edl.events:
self.assertIsNotNone(event.edits)
if event.number == 1:
self.assertEqual(len(event.edits), 1)
self.assertEqual(event.edits[0].source, "Z125C001_220217_ROLX")
self.assertEqual(event.edits[0].channels.v, True)
self.assertEqual(event.edits[0].transition.kind, "C")
self.assertEqual(event.edits[0].transition.operand, "")
self.assertEqual(event.edits[0].source_in, "15:51:58:10")
self.assertEqual(event.edits[0].record_out, "00:00:04:06")
break
def tearDown(self):
self.f.close()

View File

@@ -137,3 +137,42 @@ class TestParse(TestCase):
pycmx.Transition.Cut)
self.assertEqual(event.edits[0].source_in,
"18:44:20:12")
def test_cdl(self):
with open("tests/edls/cdl_example01.edl", "r") as f:
edl = pycmx.parse_cmx3600(f)
for event in edl.events:
if event.number == 1:
sop = event.edits[0].asc_sop
self.assertIsNotNone(sop)
assert sop
self.assertEqual(sop.slope.red, float("0.9405"))
self.assertEqual(sop.offset.green, float("-0.0276"))
sat = event.edits[0].asc_sat
self.assertIsNotNone(sat)
assert sat
self.assertEqual(sat, float('0.9640'))
def test_frmc(self):
with open("tests/edls/cdl_frmc_example01.edl", "r") as f:
edl = pycmx.parse_cmx3600(f)
for event in edl.events:
if event.number == 1:
frmc = event.edits[0].framecounts
self.assertIsNotNone(frmc)
assert frmc
self.assertEqual(frmc.start, 1001)
self.assertEqual(frmc.end, 1102)
self.assertEqual(frmc.duration, 102)
with open("tests/edls/cdl_frmc_example02.edl", "r") as f:
edl = pycmx.parse_cmx3600(f)
for event in edl.events:
if event.number == 6:
frmc = event.edits[0]._frmc_statement
self.assertIsNotNone(frmc)
assert frmc
self.assertEqual(frmc.start, 1001)
self.assertEqual(frmc.end, 1486)
self.assertEqual(frmc.duration, 486)