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
# (c) 2018-2025 Jamie Hardt
from pycmx.cdl import AscSopComponents, FramecountTriple, Rgb
from pycmx.statements import StmtCdlSat, StmtCdlSop, StmtFrmc
from .transition import Transition
from .channel_map import ChannelMap
@@ -19,14 +20,14 @@ class Edit:
trans_name_statement=None, asc_sop_statement=None,
asc_sat_statement=None, frmc_statement=None):
self.edit_statement = edit_statement
self.audio_ext = audio_ext_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
self.trans_name_statement = 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
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:
@@ -35,7 +36,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:
@@ -43,9 +44,9 @@ 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
@@ -54,19 +55,19 @@ class Edit:
Get the :obj:`Transition` object associated with this edit.
"""
if self.trans_name_statement:
return Transition(self.edit_statement.trans,
self.edit_statement.trans_op,
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:
@@ -74,7 +75,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:
@@ -82,7 +83,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:
@@ -90,7 +91,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:
@@ -98,7 +99,7 @@ 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:
@@ -138,22 +139,40 @@ class Edit:
return self.clip_name_statement.name
@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
"""
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
def asc_sat(self) -> Optional[StmtCdlSat]:
def asc_sat(self) -> Optional[str]:
"""
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
def frmc(self) -> Optional[StmtFrmc]:
def frmc(self) -> Optional[FramecountTriple]:
"""
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)
else:
return StmtFrmc(start=match.group(1), end=match.group(2),
duration=match.group(3), line_number=line_number)
try:
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:
return StmtRemark(text=line, line_number=line_number)

View File

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

View File

@@ -143,36 +143,36 @@ class TestParse(TestCase):
edl = pycmx.parse_cmx3600(f)
for event in edl.events:
if event.number == 1:
sop = event.edits[0].asc_sop_statement
sop = event.edits[0].asc_sop
self.assertIsNotNone(sop)
assert sop
self.assertEqual(sop.slope_r, "0.9405")
self.assertEqual(sop.offset_g, "-0.0276")
self.assertEqual(sop.slope.red, "0.9405")
self.assertEqual(sop.offset.green, "-0.0276")
sat = event.edits[0].asc_sat_statement
sat = event.edits[0].asc_sat
self.assertIsNotNone(sat)
assert sat
self.assertEqual(sat.value, '0.9640')
self.assertEqual(sat, '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].frmc_statement
frmc = event.edits[0]._frmc_statement
self.assertIsNotNone(frmc)
assert frmc
self.assertEqual(frmc.start, "1001")
self.assertEqual(frmc.end, "1102")
self.assertEqual(frmc.duration, "102")
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
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")
self.assertEqual(frmc.start, 1001)
self.assertEqual(frmc.end, 1486)
self.assertEqual(frmc.duration, 486)