mirror of
https://github.com/iluvcapra/pycmx.git
synced 2025-12-31 17:00:53 +00:00
Event reimplementation
Implementation of events and edits
This commit is contained in:
46
README.md
46
README.md
@@ -11,54 +11,16 @@ The `pycmx` package provides a basic interface for parsing a CMX 3600 EDL and it
|
|||||||
* Remark or comment fields with common recognized forms are read and
|
* Remark or comment fields with common recognized forms are read and
|
||||||
available to the client, including clip name and source file data.
|
available to the client, including clip name and source file data.
|
||||||
* Symbolically decodes transitions
|
* Symbolically decodes transitions
|
||||||
|
* 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.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
>>> import pycmx
|
|
||||||
>>> result = pycmx.parse_cmx3600("STP R1 v082517.edl")
|
|
||||||
>>> print(result[0:3])
|
|
||||||
[CmxEvent(title='STP_Reel 1_082517',number=1,
|
|
||||||
clip_name='FKI_LEADER_HEAD_1920X1080.MOV',
|
|
||||||
source_name='FKI_LEADER_HEAD_1920X1080.MOV',
|
|
||||||
channels=CmxChannelMap(v=True, audio_channels=set()),
|
|
||||||
transition=CmxTransition(transition='C',operand=''),
|
|
||||||
source_start='01:00:00:00',source_finish='01:00:08:00',
|
|
||||||
record_start='01:00:00:00',record_finish='01:00:08:00',
|
|
||||||
fcm_drop=False,remarks=[],line_number=2),
|
|
||||||
CmxEvent(title='STP_Reel 1_082517',number=2,
|
|
||||||
clip_name='BH_PRODUCTIONS_1.85_PRORES.MOV',
|
|
||||||
source_name='BH_PRODUCTIONS_1.85_PRORES.MOV',
|
|
||||||
channels=CmxChannelMap(v=True, audio_channels=set()),
|
|
||||||
transition=CmxTransition(transition='C',operand=''),
|
|
||||||
source_start='01:00:00:00',source_finish='01:00:14:23',
|
|
||||||
record_start='01:00:00:00',record_finish='01:00:23:00',
|
|
||||||
fcm_drop=False,remarks=[],line_number=5),
|
|
||||||
CmxEvent(title='STP_Reel 1_082517',number=3,
|
|
||||||
clip_name='V4L-1*',
|
|
||||||
source_name='B116C001_150514_R0UR',
|
|
||||||
channels=CmxChannelMap(v=True, audio_channels=set()),
|
|
||||||
transition=CmxTransition(transition='C',operand=''),
|
|
||||||
source_start='16:37:29:06',source_finish='16:37:40:22',
|
|
||||||
record_start='16:37:29:06',record_finish='01:00:50:09',
|
|
||||||
fcm_drop=False,remarks=[],line_number=8)]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Known Issues/Roadmap
|
|
||||||
|
|
||||||
To be addressed:
|
|
||||||
* Does not decode "M2" speed changes.
|
|
||||||
* Does not decode repair notes, audio notes or other Avid-specific notes.
|
|
||||||
* Does not decode Avid marker list.
|
|
||||||
|
|
||||||
May not be addressed:
|
|
||||||
|
|
||||||
* Does not parse source list at end of EDL.
|
|
||||||
|
|
||||||
Probably beyond the scope of this module:
|
|
||||||
* Does not parse timecode entries.
|
|
||||||
* Does not parse color correction notes. For this functionality we refer you to [pycdl](https://pypi.org/project/pycdl/) or [cdl-convert](https://pypi.org/project/cdl-convert/).
|
|
||||||
|
|
||||||
## Should I Use This?
|
## Should I Use This?
|
||||||
|
|
||||||
At this time, this is (at best) alpha software and the interface will be
|
At this time, this is (at best) alpha software and the interface will be
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# (c) 2018 Jamie Hardt
|
# (c) 2018 Jamie Hardt
|
||||||
|
|
||||||
from .parse_cmx_statements import (parse_cmx3600_statements,
|
from .parse_cmx_statements import (parse_cmx3600_statements,
|
||||||
StmtEvent, StmtFCM, StmtTitle)
|
StmtEvent, StmtFCM, StmtTitle, StmtClipName, StmtSourceFile, StmtAudioExt)
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
@@ -20,48 +20,113 @@ class EditList:
|
|||||||
'The title of the edit list'
|
'The title of the edit list'
|
||||||
return self.title_statement.title
|
return self.title_statement.title
|
||||||
|
|
||||||
|
@property
|
||||||
def events(self):
|
def events(self):
|
||||||
'Each event in the edit list'
|
'A generator for all the events in the edit list'
|
||||||
def events_p(statements_rest, curr_event_num,
|
is_drop = None
|
||||||
statements_event, events, is_drop):
|
current_event_num = None
|
||||||
|
event_statements = []
|
||||||
stmt = statements_rest[0]
|
for stmt in self.event_statements:
|
||||||
rem = statements_rest[1:]
|
if type(stmt) is StmtFCM:
|
||||||
|
is_drop = stmt.drop
|
||||||
|
elif type(stmt) is StmtEvent:
|
||||||
if type(stmt) is StmtEvent:
|
if current_event_num is None:
|
||||||
if stmt.event == curr_event_num:
|
current_event_num = stmt.event
|
||||||
return ( rem,curr_event_num,statements_event + [stmt],events,is_drop)
|
event_statements.append(stmt)
|
||||||
else:
|
else:
|
||||||
new_event = Event(statements_event)
|
if current_event_num != stmt.event:
|
||||||
return ( rem,stmt.event, [stmt], events + [new_event],is_drop )
|
yield Event(statements=event_statements)
|
||||||
|
event_statements = [stmt]
|
||||||
elif type(stmt) is StmtFCM:
|
current_event_num = stmt.event
|
||||||
return ( rem, curr_event_num, statements_event, events,stmt.drop)
|
|
||||||
else:
|
else:
|
||||||
return ( rem, curr_event_num, statements_event + [stmt],events, is_drop)
|
event_statements.append(stmt)
|
||||||
|
|
||||||
result = (self.event_statements, None, [], [], False)
|
|
||||||
while True:
|
|
||||||
if len(result[0]) == 0:
|
|
||||||
return result[3]
|
|
||||||
else:
|
else:
|
||||||
result = events_p(*result)
|
event_statements.append(stmt)
|
||||||
|
|
||||||
|
yield Event(statements=event_statements)
|
||||||
|
|
||||||
|
|
||||||
|
class Edit:
|
||||||
|
def __init__(self, edit_statement, audio_ext_statement, clip_name_statement, source_file_statement):
|
||||||
|
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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source(self):
|
||||||
|
return self.edit_statement.source
|
||||||
|
|
||||||
|
@property
|
||||||
|
def clip_name(self):
|
||||||
|
if self.clip_name_statement != None:
|
||||||
|
return self.clip_name_statement.name
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
Edit = namedtuple("Edit","channels transition source_ref source_start source_finish record_start record_finish")
|
|
||||||
|
|
||||||
|
|
||||||
class Event:
|
class Event:
|
||||||
def __init__(self, statements):
|
def __init__(self, statements):
|
||||||
self.statements = statements
|
self.statements = statements
|
||||||
|
|
||||||
def number():
|
@property
|
||||||
return statements[0].event
|
def number(self):
|
||||||
|
return self._edit_statements()[0].event
|
||||||
|
|
||||||
|
@property
|
||||||
|
def edits(self):
|
||||||
|
edits_audio = list( self._statements_with_audio_ext() )
|
||||||
|
clip_names = self._clip_name_statements()
|
||||||
|
source_files= self._source_file_statements()
|
||||||
|
|
||||||
|
the_zip = [edits_audio]
|
||||||
|
|
||||||
|
if len(edits_audio) == 2:
|
||||||
|
cn = [None, None]
|
||||||
|
for clip_name in clip_names:
|
||||||
|
if clip_name.affect == 'from':
|
||||||
|
cn[0] = clip_name
|
||||||
|
elif clip_name.affect == 'to':
|
||||||
|
cn[1] = clip_name
|
||||||
|
|
||||||
|
the_zip.append(cn)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if len(edits_audio) == len(clip_names):
|
||||||
|
the_zip.append(clip_names)
|
||||||
|
else:
|
||||||
|
the_zip.append([None] * len(edits_audio) )
|
||||||
|
|
||||||
|
if len(edits_audio) == len(source_files):
|
||||||
|
the_zip.append(source_files)
|
||||||
|
elif len(source_files) == 1:
|
||||||
|
the_zip.append( source_files * len(edits_audio) )
|
||||||
|
else:
|
||||||
|
the_zip.append([None] * len(edits_audio) )
|
||||||
|
|
||||||
|
|
||||||
|
return [ Edit(e1[0],e1[1],n1,s1) for (e1,n1,s1) in zip(*the_zip) ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _edit_statements(self):
|
||||||
|
return [s for s in self.statements if type(s) is StmtEvent]
|
||||||
|
|
||||||
|
def _clip_name_statements(self):
|
||||||
|
return [s for s in self.statements if type(s) is StmtClipName]
|
||||||
|
|
||||||
|
def _source_file_statements(self):
|
||||||
|
return [s for s in self.statements if type(s) is StmtSourceFile]
|
||||||
|
|
||||||
|
def _statements_with_audio_ext(self):
|
||||||
|
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 edits():
|
|
||||||
for statement in self.statements:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ StmtFCM = namedtuple("FCM",["drop","line_number"])
|
|||||||
StmtEvent = namedtuple("Event",["event","source","channels","trans",\
|
StmtEvent = namedtuple("Event",["event","source","channels","trans",\
|
||||||
"trans_op","source_in","source_out","record_in","record_out","line_number"])
|
"trans_op","source_in","source_out","record_in","record_out","line_number"])
|
||||||
StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"])
|
StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"])
|
||||||
StmtClipName = namedtuple("ClipName",["name","line_number"])
|
StmtClipName = namedtuple("ClipName",["name","affect","line_number"])
|
||||||
StmtSourceFile = namedtuple("SourceFile",["filename","line_number"])
|
StmtSourceFile = namedtuple("SourceFile",["filename","line_number"])
|
||||||
StmtRemark = namedtuple("Remark",["text","line_number"])
|
StmtRemark = namedtuple("Remark",["text","line_number"])
|
||||||
StmtEffectsName = namedtuple("EffectsName",["name","line_number"])
|
StmtEffectsName = namedtuple("EffectsName",["name","line_number"])
|
||||||
@@ -109,7 +109,9 @@ def parse_extended_audio_channels(line, line_number):
|
|||||||
|
|
||||||
def parse_remark(line, line_number):
|
def parse_remark(line, line_number):
|
||||||
if line.startswith("FROM CLIP NAME:"):
|
if line.startswith("FROM CLIP NAME:"):
|
||||||
return StmtClipName(name=line[15:].strip() , line_number=line_number)
|
return StmtClipName(name=line[15:].strip() , affect="from", line_number=line_number)
|
||||||
|
elif line.startswith("TO CLIP NAME:"):
|
||||||
|
return StmtClipName(name=line[13:].strip(), affect="to", line_number=line_number)
|
||||||
elif line.startswith("SOURCE FILE:"):
|
elif line.startswith("SOURCE FILE:"):
|
||||||
return StmtSourceFile(filename=line[12:].strip() , line_number=line_number)
|
return StmtSourceFile(filename=line[12:].strip() , line_number=line_number)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -16,8 +16,24 @@ class TestParse(TestCase):
|
|||||||
|
|
||||||
for fn, count in zip(files, counts):
|
for fn, count in zip(files, counts):
|
||||||
edl = pycmx.parse_cmx3600(f"tests/edls/{fn}" )
|
edl = pycmx.parse_cmx3600(f"tests/edls/{fn}" )
|
||||||
actual = len(edl.events())
|
actual = len(list(edl.events ))
|
||||||
self.assertTrue( actual == count , f"expected {count} but found {actual}")
|
self.assertTrue( actual == count , f"expected {count} in file {fn} but found {actual}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_events(self):
|
||||||
|
|
||||||
|
edl = pycmx.parse_cmx3600("tests/edls/TEST.edl")
|
||||||
|
events = list( edl.events )
|
||||||
|
|
||||||
|
self.assertEqual( int(events[0].number) , 1)
|
||||||
|
self.assertEqual( events[0].edits[0].source , "OY_HEAD_")
|
||||||
|
self.assertEqual( events[0].edits[0].clip_name , "HEAD LEADER MONO")
|
||||||
|
|
||||||
|
self.assertEqual( int(events[42].number) , 43)
|
||||||
|
self.assertEqual( events[42].edits[0].source , "TC_R1_V1")
|
||||||
|
self.assertEqual( events[42].edits[0].clip_name , "TC R1 V1.2 TEMP1 FX ST.WAV")
|
||||||
|
|
||||||
|
self.assertEqual( events[42].edits[1].source , "TC_R1_V6")
|
||||||
|
self.assertEqual( events[42].edits[1].clip_name , "TC R1 V6 TEMP2 ST FX.WAV")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user