retyping some CDL items

This commit is contained in:
2025-12-16 15:57:34 -08:00
parent 5c02d09a7a
commit 3cdae1761f
5 changed files with 97 additions and 42 deletions

31
pycmx/cdl.py Normal file
View File

@@ -0,0 +1,31 @@
# pycmx
# (c) 2025 Jamie Hardt
from typing import Generic, NamedTuple, TypeVar
T = TypeVar('T')
class Rgb(NamedTuple, Generic[T]):
red: T
green: T
blue: T
class AscSopComponents(NamedTuple, Generic[T]):
"""
Fields in an ASC SOP (Slope-Offset-Power) color transfer function
statement
"""
slope: Rgb[T]
offset: Rgb[T]
power: Rgb[T]
class FramecountTriple(NamedTuple):
"""
Fields in an FRMC statement
"""
start: int
end: int
duration: int

View File

@@ -1,6 +1,7 @@
# pycmx # pycmx
# (c) 2018-2025 Jamie Hardt # (c) 2018-2025 Jamie Hardt
from pycmx.cdl import AscSopComponents, FramecountTriple, Rgb
from pycmx.statements import StmtCdlSat, StmtCdlSop, StmtFrmc from pycmx.statements import StmtCdlSat, StmtCdlSop, StmtFrmc
from .transition import Transition from .transition import Transition
from .channel_map import ChannelMap from .channel_map import ChannelMap
@@ -19,14 +20,14 @@ class Edit:
trans_name_statement=None, asc_sop_statement=None, trans_name_statement=None, asc_sop_statement=None,
asc_sat_statement=None, frmc_statement=None): asc_sat_statement=None, frmc_statement=None):
self.edit_statement = edit_statement self._edit_statement = edit_statement
self.audio_ext = audio_ext_statement self._audio_ext = audio_ext_statement
self.clip_name_statement = clip_name_statement self.clip_name_statement = clip_name_statement
self.source_file_statement = source_file_statement self.source_file_statement = source_file_statement
self.trans_name_statement = trans_name_statement self.trans_name_statement = trans_name_statement
self.asc_sop_statement: Optional[StmtCdlSop] = asc_sop_statement self._asc_sop_statement: Optional[StmtCdlSop] = asc_sop_statement
self.asc_sat_statement: Optional[StmtCdlSat] = asc_sat_statement self._asc_sat_statement: Optional[StmtCdlSat] = asc_sat_statement
self.frmc_statement: Optional[StmtFrmc] = frmc_statement self._frmc_statement: Optional[StmtFrmc] = frmc_statement
@property @property
def line_number(self) -> int: def line_number(self) -> int:
@@ -35,7 +36,7 @@ class Edit:
this edit. Line numbers a zero-indexed, such that the this edit. Line numbers a zero-indexed, such that the
"TITLE:" record is line zero. "TITLE:" record is line zero.
""" """
return self.edit_statement.line_number return self._edit_statement.line_number
@property @property
def channels(self) -> ChannelMap: def channels(self) -> ChannelMap:
@@ -43,9 +44,9 @@ class Edit:
Get the :obj:`ChannelMap` object associated with this Edit. Get the :obj:`ChannelMap` object associated with this Edit.
""" """
cm = ChannelMap() cm = ChannelMap()
cm._append_event(self.edit_statement.channels) cm._append_event(self._edit_statement.channels)
if self.audio_ext is not None: if self._audio_ext is not None:
cm._append_ext(self.audio_ext) cm._append_ext(self._audio_ext)
return cm return cm
@property @property
@@ -54,19 +55,19 @@ class Edit:
Get the :obj:`Transition` object associated with this edit. Get the :obj:`Transition` object associated with this edit.
""" """
if self.trans_name_statement: if self.trans_name_statement:
return Transition(self.edit_statement.trans, return Transition(self._edit_statement.trans,
self.edit_statement.trans_op, self._edit_statement.trans_op,
self.trans_name_statement.name) self.trans_name_statement.name)
else: else:
return Transition(self.edit_statement.trans, return Transition(self._edit_statement.trans,
self.edit_statement.trans_op, None) self._edit_statement.trans_op, None)
@property @property
def source_in(self) -> str: def source_in(self) -> str:
""" """
Get the source in timecode. Get the source in timecode.
""" """
return self.edit_statement.source_in return self._edit_statement.source_in
@property @property
def source_out(self) -> str: def source_out(self) -> str:
@@ -74,7 +75,7 @@ class Edit:
Get the source out timecode. Get the source out timecode.
""" """
return self.edit_statement.source_out return self._edit_statement.source_out
@property @property
def record_in(self) -> str: def record_in(self) -> str:
@@ -82,7 +83,7 @@ class Edit:
Get the record in timecode. Get the record in timecode.
""" """
return self.edit_statement.record_in return self._edit_statement.record_in
@property @property
def record_out(self) -> str: def record_out(self) -> str:
@@ -90,7 +91,7 @@ class Edit:
Get the record out timecode. Get the record out timecode.
""" """
return self.edit_statement.record_out return self._edit_statement.record_out
@property @property
def source(self) -> str: def source(self) -> str:
@@ -98,7 +99,7 @@ class Edit:
Get the source column. This is the 8, 32 or 128-character string on the 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. event record line, this usually references the tape name of the source.
""" """
return self.edit_statement.source return self._edit_statement.source
@property @property
def black(self) -> bool: def black(self) -> bool:
@@ -138,22 +139,40 @@ class Edit:
return self.clip_name_statement.name return self.clip_name_statement.name
@property @property
def asc_sop(self) -> Optional[StmtCdlSop]: def asc_sop(self) -> Optional[AscSopComponents[str]]:
""" """
Get ASC CDL Slope-Offset-Power transfer function for clip, if present Get ASC CDL Slope-Offset-Power transfer function for clip, if present
""" """
return self.asc_sop_statement if self._asc_sop_statement is None:
return None
s = self._asc_sop_statement
return AscSopComponents(
slope=Rgb(red=s.slope_r, green=s.slope_g,
blue=s.slope_b),
offset=Rgb(red=s.offset_r, green=s.offset_g,
blue=s.offset_g),
power=Rgb(red=s.power_r, green=s.power_g,
blue=s.power_b)
)
@property @property
def asc_sat(self) -> Optional[StmtCdlSat]: def asc_sat(self) -> Optional[str]:
""" """
Get ASC CDL saturation value for clip, if present Get ASC CDL saturation value for clip, if present
""" """
return self.asc_sat_statement if self._asc_sat_statement is None:
return None
return self._asc_sat_statement.value
@property @property
def frmc(self) -> Optional[StmtFrmc]: def frmc(self) -> Optional[FramecountTriple]:
""" """
Get FRMC data Get FRMC data
""" """
return self.frmc_statement if not self._frmc_statement:
return None
return FramecountTriple(int())

View File

@@ -141,8 +141,13 @@ def _parse_remark(line, line_number) -> object:
return StmtRemark(line, line_number) return StmtRemark(line, line_number)
else: else:
return StmtFrmc(start=match.group(1), end=match.group(2), try:
duration=match.group(3), line_number=line_number) return StmtFrmc(start=int(match.group(1)),
end=int(match.group(2)),
duration=int(match.group(3)),
line_number=line_number)
except ValueError:
return StmtRemark(line, line_number)
else: else:
return StmtRemark(text=line, line_number=line_number) return StmtRemark(text=line, line_number=line_number)

View File

@@ -66,9 +66,9 @@ class StmtCdlSat(NamedTuple):
class StmtFrmc(NamedTuple): class StmtFrmc(NamedTuple):
start: str start: int
end: str end: int
duration: str duration: int
line_number: int line_number: int

View File

@@ -143,36 +143,36 @@ class TestParse(TestCase):
edl = pycmx.parse_cmx3600(f) edl = pycmx.parse_cmx3600(f)
for event in edl.events: for event in edl.events:
if event.number == 1: if event.number == 1:
sop = event.edits[0].asc_sop_statement sop = event.edits[0].asc_sop
self.assertIsNotNone(sop) self.assertIsNotNone(sop)
assert sop assert sop
self.assertEqual(sop.slope_r, "0.9405") self.assertEqual(sop.slope.red, "0.9405")
self.assertEqual(sop.offset_g, "-0.0276") self.assertEqual(sop.offset.green, "-0.0276")
sat = event.edits[0].asc_sat_statement sat = event.edits[0].asc_sat
self.assertIsNotNone(sat) self.assertIsNotNone(sat)
assert sat assert sat
self.assertEqual(sat.value, '0.9640') self.assertEqual(sat, '0.9640')
def test_frmc(self): def test_frmc(self):
with open("tests/edls/cdl_frmc_example01.edl", "r") as f: with open("tests/edls/cdl_frmc_example01.edl", "r") as f:
edl = pycmx.parse_cmx3600(f) edl = pycmx.parse_cmx3600(f)
for event in edl.events: for event in edl.events:
if event.number == 1: if event.number == 1:
frmc = event.edits[0].frmc_statement frmc = event.edits[0]._frmc_statement
self.assertIsNotNone(frmc) self.assertIsNotNone(frmc)
assert frmc assert frmc
self.assertEqual(frmc.start, "1001") self.assertEqual(frmc.start, 1001)
self.assertEqual(frmc.end, "1102") self.assertEqual(frmc.end, 1102)
self.assertEqual(frmc.duration, "102") self.assertEqual(frmc.duration, 102)
with open("tests/edls/cdl_frmc_example02.edl", "r") as f: with open("tests/edls/cdl_frmc_example02.edl", "r") as f:
edl = pycmx.parse_cmx3600(f) edl = pycmx.parse_cmx3600(f)
for event in edl.events: for event in edl.events:
if event.number == 6: if event.number == 6:
frmc = event.edits[0].frmc_statement frmc = event.edits[0]._frmc_statement
self.assertIsNotNone(frmc) self.assertIsNotNone(frmc)
assert frmc assert frmc
self.assertEqual(frmc.start, "1001") self.assertEqual(frmc.start, 1001)
self.assertEqual(frmc.end, "1486") self.assertEqual(frmc.end, 1486)
self.assertEqual(frmc.duration, "486") self.assertEqual(frmc.duration, 486)