mirror of
https://github.com/iluvcapra/pycmx.git
synced 2026-10-02 01:37:19 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49a536f0cb | |||
| b00121033c | |||
| a200e41da7 | |||
| 4f0a9db555 | |||
| d61adad4c3 | |||
| 03b4119ccf | |||
| 5e560dfa0e | |||
| daedabd7cc | |||
| 9fd5d36611 | |||
| e8556958b5 | |||
| 98e21e7a09 | |||
| 694b99d081 | |||
| 2e4a0439c1 | |||
| 688402d195 | |||
| 4e81810584 | |||
| faf2596a57 | |||
| 1e9fbe339c | |||
| a8d00470d4 | |||
| fe1e59e731 | |||
| ec8a08074d | |||
| ef683a7683 | |||
| d778f64230 |
@@ -1,5 +0,0 @@
|
||||
[flake8]
|
||||
per-file-ignores =
|
||||
src/pycmx/__init__.py: F401
|
||||
tests/__init__.py: F401
|
||||
|
||||
@@ -27,11 +27,9 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e .[dev]
|
||||
- name: Lint with flake8
|
||||
- name: Lint with ruff
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
flake8 . --count --max-line-length=79 --statistics
|
||||
ruff check src/
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest
|
||||
|
||||
+2
-2
@@ -7,13 +7,13 @@ version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-lts-latest
|
||||
tools:
|
||||
python: "3.13"
|
||||
jobs:
|
||||
install:
|
||||
- pip install --upgrade pip
|
||||
- pip install --group 'doc'
|
||||
- pip install --group 'doc' -e .
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
|
||||
+32
-1
@@ -9,5 +9,36 @@ The best way to contribute code to this project is to find this project on [Gith
|
||||
If you have EDLs you are having trouble working with becuase of unusual formatting, please send me a copy! Contact us
|
||||
through [Github].
|
||||
|
||||
|
||||
[github]: https://github.com/iluvcapra/pycmx
|
||||
|
||||
## Regarding use of Agents
|
||||
|
||||
`pycmx` is an open-source project that is offered free for no commerical gain, and
|
||||
is developed and maintained for educational and creative reasons.
|
||||
|
||||
If you use an agent or LLM to produce code for it you are missing out on the benefits
|
||||
of contributing to an open-source project, particularly community, collaboration with
|
||||
other developers and designers, and being able to learn and experiment without the
|
||||
burden of deadlines or worrying about business cases or profits.
|
||||
|
||||
This project is supposed to be fun, do not let machines have fun for you.
|
||||
|
||||
We can't prevent you from using LLMs to contribute to this project but we ask you
|
||||
abide by the following eitiquette when doing so:
|
||||
|
||||
* All communication with the maintainers must be written by a human in their own
|
||||
voice. Never use an LLM to craft thread comments, discussion posts, issues, emails
|
||||
or other correspondence with other developers or the maintainers.
|
||||
* PRs must be submitted by a person. Do not allow an agent to submit its own PRs to
|
||||
this project.
|
||||
* Especially if you are a new contributor to this project, please submit only one PR
|
||||
at a time and please restrict the subject matter of the PR to a specific unit,
|
||||
module or tool. All submissions have to be reviewed and understood by the
|
||||
maintainers before they can be merged.
|
||||
|
||||
Obviously we can't verify if you follow all of these rules but certain telltale
|
||||
traits of LLM-predicted text or code will raise a flag: lack of brevity in
|
||||
descriptions or code comments, large amounts of text describing your process or
|
||||
steps that add little to understanding the changes you've made, use of an
|
||||
obsequious tone or being excessively accomodating, immediately doing requests
|
||||
without further discussion or clarifications.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2022 Jamie Hardt.
|
||||
Copyright (c) 2026 Jamie Hardt.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
+56
-35
@@ -1,12 +1,11 @@
|
||||
import pycmx
|
||||
import re
|
||||
import argparse
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
|
||||
import logging
|
||||
import pycmx
|
||||
|
||||
|
||||
FORMAT = '%(asctime)-15s %(message)s'
|
||||
FORMAT = "%(asctime)-15s %(message)s"
|
||||
logging.basicConfig(format=FORMAT)
|
||||
|
||||
|
||||
@@ -21,7 +20,7 @@ def all_video_edits(edl):
|
||||
|
||||
|
||||
def get_scene_name(edit, pattern):
|
||||
scene_extractor = re.compile(pattern, re.I)
|
||||
scene_extractor = re.compile(pattern, re.IGNORECASE)
|
||||
if edit.clip_name is None:
|
||||
return None
|
||||
else:
|
||||
@@ -37,16 +36,15 @@ def output_cmx(outfile, out_list):
|
||||
outfile.write("FCM: NON-DROP FRAME\r\n")
|
||||
|
||||
for i, o in enumerate(out_list):
|
||||
line = '%03i AX V C ' % (i)
|
||||
line += '00:00:00:00 00:00:00:00 %s %s\r\n' % (o['start'], o['end'])
|
||||
line = "{i: 03d} AX V C "
|
||||
line += f"00:00:00:00 00:00:00:00 {o['start']} {o['end']}\r\n"
|
||||
outfile.write(line)
|
||||
outfile.write("* FROM CLIP NAME: %s\r\n" % (o['scene']))
|
||||
outfile.write(f"* FROM CLIP NAME: {o['scene']}\r\n")
|
||||
|
||||
|
||||
def output_cols(outfile, out_list):
|
||||
for o in out_list:
|
||||
outfile.write("%-12s\t%-12s\t%s\n" %
|
||||
(o['start'], o['end'], o['scene']))
|
||||
outfile.write(f"{o['start']:>12}\t{o['end']:>12}\t{o['scene']}")
|
||||
|
||||
|
||||
def scene_list(infile, outfile, out_format, pattern):
|
||||
@@ -68,15 +66,17 @@ def scene_list(infile, outfile, out_format, pattern):
|
||||
|
||||
out_list = []
|
||||
for group in grouped_edits:
|
||||
out_list.append({
|
||||
'start': group[0].record_in,
|
||||
'end': group[-1].record_out,
|
||||
'scene': get_scene_name(group[0], pattern)}
|
||||
out_list.append(
|
||||
{
|
||||
"start": group[0].record_in,
|
||||
"end": group[-1].record_out,
|
||||
"scene": get_scene_name(group[0], pattern),
|
||||
}
|
||||
)
|
||||
|
||||
if out_format == 'cmx':
|
||||
if out_format == "cmx":
|
||||
output_cmx(outfile, out_list)
|
||||
if out_format == 'cols':
|
||||
if out_format == "cols":
|
||||
output_cols(outfile, out_list)
|
||||
else:
|
||||
log.warning(f"Format {out_format} unrecognized. Will use cmx.\n")
|
||||
@@ -85,28 +85,49 @@ def scene_list(infile, outfile, out_format, pattern):
|
||||
|
||||
def scene_list_cli():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Read video events from an input CMX EDL and output '
|
||||
'events merged into scenes.')
|
||||
parser.add_argument('-o', '--outfile', default=sys.stdout,
|
||||
type=argparse.FileType('w'),
|
||||
help='Output file. Default is stdout.')
|
||||
parser.add_argument('-f', '--format', default='cmx', type=str,
|
||||
help='Output format. Options are cols and cmx, cmx '
|
||||
'is the default.')
|
||||
parser.add_argument('-p', '--pattern', default='V?([A-Z]*[0-9]+)',
|
||||
help='RE pattern for extracting scene name from clip '
|
||||
'name. The default is "V?([A-Z]*[0-9]+)". ' +
|
||||
'This pattern will be matched case-insensitively.')
|
||||
parser.add_argument('input_edl', default=sys.stdin,
|
||||
type=argparse.FileType('r'), nargs='?',
|
||||
help='Input file. Default is stdin.')
|
||||
description="Read video events from an input CMX EDL and output "
|
||||
"events merged into scenes."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--outfile",
|
||||
default=sys.stdout,
|
||||
type=argparse.FileType("w"),
|
||||
help="Output file. Default is stdout.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--format",
|
||||
default="cmx",
|
||||
type=str,
|
||||
help="Output format. Options are cols and cmx, cmx is the default.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--pattern",
|
||||
default="V?([A-Z]*[0-9]+)",
|
||||
help="RE pattern for extracting scene name from clip "
|
||||
'name. The default is "V?([A-Z]*[0-9]+)". '
|
||||
+ "This pattern will be matched case-insensitively.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"input_edl",
|
||||
default=sys.stdin,
|
||||
type=argparse.FileType("r"),
|
||||
nargs="?",
|
||||
help="Input file. Default is stdin.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
infile = args.input_edl
|
||||
|
||||
scene_list(infile=infile, outfile=args.outfile,
|
||||
out_format=args.format, pattern=args.pattern)
|
||||
scene_list(
|
||||
infile=infile,
|
||||
outfile=args.outfile,
|
||||
out_format=args.format,
|
||||
pattern=args.pattern,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
scene_list_cli()
|
||||
|
||||
+30
-31
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
@@ -15,14 +14,15 @@
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = u'pycmx'
|
||||
copyright = u'(c) 2018-2025, Jamie Hardt'
|
||||
author = u'Jamie Hardt'
|
||||
project = "pycmx"
|
||||
copyright = "(c) 2018-2026, Jamie Hardt"
|
||||
author = "Jamie Hardt"
|
||||
|
||||
release = importlib.metadata.version("pycmx")
|
||||
version = release
|
||||
@@ -40,31 +40,31 @@ version = release
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.githubpages',
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.githubpages",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = 'em'
|
||||
language = "em"
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
@@ -80,18 +80,18 @@ pygments_style = None
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
html_theme_options = {'collapse_navigation': False}
|
||||
html_theme_options = {"collapse_navigation": False}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
@@ -107,7 +107,7 @@ html_static_path = ['_static']
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'pycmxdoc'
|
||||
htmlhelp_basename = "pycmxdoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
@@ -116,15 +116,12 @@ latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
@@ -134,8 +131,7 @@ latex_elements = {
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'pycmx.tex', u'pycmx Documentation',
|
||||
u'Jamie Hardt', 'manual'),
|
||||
(master_doc, "pycmx.tex", "pycmx Documentation", "Jamie Hardt", "manual"),
|
||||
]
|
||||
|
||||
|
||||
@@ -143,10 +139,7 @@ latex_documents = [
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'pycmx', u'pycmx Documentation',
|
||||
[author], "3p")
|
||||
]
|
||||
man_pages = [(master_doc, "pycmx", "pycmx Documentation", [author], "3p")]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
@@ -155,9 +148,15 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'pycmx', u'pycmx Documentation',
|
||||
author, 'pycmx', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
master_doc,
|
||||
"pycmx",
|
||||
"pycmx Documentation",
|
||||
author,
|
||||
"pycmx",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -176,7 +175,7 @@ epub_title = project
|
||||
# epub_uid = ''
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
epub_exclude_files = ["search.html"]
|
||||
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
|
||||
@@ -16,7 +16,7 @@ The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
|
||||
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
|
||||
* A 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
|
||||
|
||||
+16
-18
@@ -1,8 +1,8 @@
|
||||
[project]
|
||||
name = "pycmx"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
description = "Python CMX 3600 Edit Decision List Parser"
|
||||
authors = [{name = "Jamie Hardt", email= "<jamiehardt@me.com>"}]
|
||||
authors = [{name = "Jamie Hardt", email= "jamiehardt@me.com"}]
|
||||
license-files = ["LICENSE"]
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
@@ -13,7 +13,6 @@ keywords = [
|
||||
requires-python = '>3.8'
|
||||
classifiers = [
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Topic :: Multimedia',
|
||||
'Topic :: Multimedia :: Video',
|
||||
'Topic :: Text Processing',
|
||||
@@ -29,11 +28,11 @@ classifiers = [
|
||||
[project.optional-dependencies]
|
||||
doc = [
|
||||
'sphinx >= 5.3.0',
|
||||
'sphinx_rtd_theme >= 1.1.1'
|
||||
'sphinx_rtd_theme >= 1.1.1',
|
||||
]
|
||||
dev = [
|
||||
'flake8',
|
||||
'pytest'
|
||||
'pytest',
|
||||
'ruff>=0.16.8'
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -43,24 +42,23 @@ Repository = "https://github.com/iluvcapra/pycmx.git"
|
||||
Tracker = "https://github.com/iluvcapra/pycmx/issues"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ['ruff', 'pytest']
|
||||
doc = ['sphinx', 'sphinx_rtd_theme']
|
||||
|
||||
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "basic"
|
||||
|
||||
[tool.pylint]
|
||||
max-line-length = 88
|
||||
disable = [
|
||||
"C0103", # (invalid-name)
|
||||
"C0114", # (missing-module-docstring)
|
||||
"C0115", # (missing-class-docstring)
|
||||
"C0116", # (missing-function-docstring)
|
||||
"R0903", # (too-few-public-methods)
|
||||
"R0913", # (too-many-arguments)
|
||||
"W0105", # (pointless-string-statement)
|
||||
]
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
|
||||
[tool.ruff.lint]
|
||||
# select = ["E", "F", "W"]
|
||||
|
||||
[tool.ruff.format]
|
||||
docstring-code-line-length = 88
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.9.18,<0.10.0"]
|
||||
requires = ["uv_build>=0.9.18,<0.12"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
pycmx is a parser for CMX 3600-style EDLs.
|
||||
|
||||
@@ -7,7 +6,9 @@ copy and reuse this software, refer to the LICENSE file included with the
|
||||
distribution.
|
||||
"""
|
||||
|
||||
from .edit import Edit
|
||||
from .event import Event
|
||||
from .parse_cmx_events import parse_cmx3600
|
||||
from .transition import Transition
|
||||
from .event import Event
|
||||
from .edit import Edit
|
||||
|
||||
__all__ = ("Edit", "Event", "Transition", "parse_cmx3600")
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, NamedTuple, TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
+15
-15
@@ -1,8 +1,10 @@
|
||||
# pycmx
|
||||
# (c) 2018-2025 Jamie Hardt
|
||||
|
||||
from re import (compile, match)
|
||||
from typing import Dict, Tuple, Generator
|
||||
from __future__ import annotations
|
||||
|
||||
from re import compile, match
|
||||
from typing import Generator
|
||||
|
||||
|
||||
class ChannelMap:
|
||||
@@ -10,35 +12,33 @@ class ChannelMap:
|
||||
Represents a set of all the channels to which an event applies.
|
||||
"""
|
||||
|
||||
_chan_map: Dict[str, Tuple] = {
|
||||
def __init__(self, v=False, audio_channels=None):
|
||||
self._audio_channel_set = audio_channels or set()
|
||||
self.v = v
|
||||
self._chan_map: dict[str, tuple] = {
|
||||
"V": (True, False, False),
|
||||
"A": (False, True, False),
|
||||
"A2": (False, False, True),
|
||||
"AA": (False, True, True),
|
||||
"B": (True, True, False),
|
||||
"AA/V": (True, True, True),
|
||||
"A2/V": (True, False, True)
|
||||
"A2/V": (True, False, True),
|
||||
}
|
||||
|
||||
def __init__(self, v=False, audio_channels=set()):
|
||||
self._audio_channel_set = audio_channels
|
||||
self.v = v
|
||||
|
||||
@property
|
||||
def video(self) -> bool:
|
||||
'True if video is included'
|
||||
"True if video is included"
|
||||
return self.v
|
||||
|
||||
@property
|
||||
def audio(self) -> bool:
|
||||
'True if an audio channel is included'
|
||||
"True if an audio channel is included"
|
||||
return len(self._audio_channel_set) > 0
|
||||
|
||||
@property
|
||||
def channels(self) -> Generator[int, None, None]:
|
||||
'A generator for each audio channel'
|
||||
for c in self._audio_channel_set:
|
||||
yield c
|
||||
"A generator for each audio channel"
|
||||
yield from self._audio_channel_set
|
||||
|
||||
@property
|
||||
def a1(self) -> bool:
|
||||
@@ -78,7 +78,7 @@ class ChannelMap:
|
||||
|
||||
def get_audio_channel(self, chan_num) -> bool:
|
||||
"""True if chan_num is included"""
|
||||
return (chan_num in self._audio_channel_set)
|
||||
return chan_num in self._audio_channel_set
|
||||
|
||||
def set_audio_channel(self, chan_num, enabled: bool):
|
||||
"""If enabled is true, chan_num will be included"""
|
||||
@@ -88,7 +88,7 @@ class ChannelMap:
|
||||
self._audio_channel_set.remove(chan_num)
|
||||
|
||||
def _append_event(self, event_str):
|
||||
alt_channel_re = compile(r'^A(\d+)')
|
||||
alt_channel_re = compile(r"^A(\d+)")
|
||||
if event_str in self._chan_map:
|
||||
channels = self._chan_map[event_str]
|
||||
self.v = channels[0]
|
||||
|
||||
+27
-29
@@ -1,21 +1,21 @@
|
||||
# pycmx
|
||||
# (c) 2018-2025 Jamie Hardt
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .cdl import AscSopComponents, FramecountTriple
|
||||
from .channel_map import ChannelMap
|
||||
from .statements import (
|
||||
StmtAudioExt,
|
||||
StmtCdlSat,
|
||||
StmtCdlSop,
|
||||
StmtFrmc,
|
||||
StmtEvent,
|
||||
StmtAudioExt,
|
||||
StmtClipName,
|
||||
StmtSourceFile,
|
||||
StmtEffectsName,
|
||||
StmtEvent,
|
||||
StmtFrmc,
|
||||
StmtSourceFile,
|
||||
)
|
||||
from .transition import Transition
|
||||
from .channel_map import ChannelMap
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Edit:
|
||||
@@ -27,24 +27,22 @@ class Edit:
|
||||
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,
|
||||
audio_ext_statement: StmtAudioExt | None,
|
||||
clip_name_statement: StmtClipName | None,
|
||||
source_file_statement: StmtSourceFile | None,
|
||||
trans_name_statement: StmtEffectsName | None = None,
|
||||
asc_sop_statement: StmtCdlSop | None = None,
|
||||
asc_sat_statement: StmtCdlSat | None = None,
|
||||
frmc_statement: StmtFrmc | None = None,
|
||||
) -> None:
|
||||
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
|
||||
self._audio_ext: StmtAudioExt | None = audio_ext_statement
|
||||
self._clip_name_statement: StmtClipName | None = clip_name_statement
|
||||
self._source_file_statement: StmtSourceFile | None = source_file_statement
|
||||
self._trans_name_statement: StmtEffectsName | None = trans_name_statement
|
||||
self._asc_sop_statement: StmtCdlSop | None = asc_sop_statement
|
||||
self._asc_sat_statement: StmtCdlSat | None = asc_sat_statement
|
||||
self._frmc_statement: StmtFrmc | None = frmc_statement
|
||||
|
||||
@property
|
||||
def line_number(self) -> int:
|
||||
@@ -138,7 +136,7 @@ class Edit:
|
||||
return self.source == "AX"
|
||||
|
||||
@property
|
||||
def source_file(self) -> Optional[str]:
|
||||
def source_file(self) -> str | None:
|
||||
"""
|
||||
Get the source file, as attested by a "* SOURCE FILE" remark on the
|
||||
EDL. This will return None if the information is not present.
|
||||
@@ -149,7 +147,7 @@ class Edit:
|
||||
return self._source_file_statement.filename
|
||||
|
||||
@property
|
||||
def clip_name(self) -> Optional[str]:
|
||||
def clip_name(self) -> str | None:
|
||||
"""
|
||||
Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP
|
||||
NAME" remark on the EDL. This will return None if the information is
|
||||
@@ -161,7 +159,7 @@ class Edit:
|
||||
return self._clip_name_statement.name
|
||||
|
||||
@property
|
||||
def asc_sop(self) -> Optional[AscSopComponents[float]]:
|
||||
def asc_sop(self) -> AscSopComponents[float] | None:
|
||||
"""
|
||||
Get ASC CDL Slope-Offset-Power color transfer function for the edit,
|
||||
if present. The ASC SOP is a transfer function of the form:
|
||||
@@ -177,7 +175,7 @@ class Edit:
|
||||
return self._asc_sop_statement.cdl_sop
|
||||
|
||||
@property
|
||||
def asc_sop_raw(self) -> Optional[str]:
|
||||
def asc_sop_raw(self) -> str | None:
|
||||
"""
|
||||
ASC CDL Slope-Offset-Power statement raw line.
|
||||
"""
|
||||
@@ -187,7 +185,7 @@ class Edit:
|
||||
return self._asc_sop_statement.line
|
||||
|
||||
@property
|
||||
def asc_sat(self) -> Optional[float]:
|
||||
def asc_sat(self) -> float | None:
|
||||
"""
|
||||
Get ASC CDL saturation value for clip, if present.
|
||||
"""
|
||||
@@ -197,7 +195,7 @@ class Edit:
|
||||
return self._asc_sat_statement.value
|
||||
|
||||
@property
|
||||
def framecounts(self) -> Optional[FramecountTriple]:
|
||||
def framecounts(self) -> FramecountTriple | None:
|
||||
"""
|
||||
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
|
||||
|
||||
+20
-12
@@ -1,13 +1,20 @@
|
||||
# pycmx
|
||||
# (c) 2018-2025 Jamie Hardt
|
||||
|
||||
from .statements import (StmtCorruptRemark, StmtTitle, StmtEvent,
|
||||
StmtUnrecognized, StmtSourceUMID)
|
||||
from .event import Event
|
||||
from .channel_map import ChannelMap
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Generator
|
||||
|
||||
from .channel_map import ChannelMap
|
||||
from .event import Event
|
||||
from .statements import (
|
||||
StmtCorruptRemark,
|
||||
StmtEvent,
|
||||
StmtSourceUMID,
|
||||
StmtTitle,
|
||||
StmtUnrecognized,
|
||||
)
|
||||
|
||||
|
||||
class EditList:
|
||||
"""
|
||||
@@ -28,19 +35,20 @@ class EditList:
|
||||
Adobe EDLs with more than 999 events will be reported as "3600".
|
||||
"""
|
||||
first_event = next(
|
||||
(s for s in self.event_statements if type(s) is StmtEvent), None)
|
||||
(s for s in self.event_statements if type(s) is StmtEvent), None
|
||||
)
|
||||
|
||||
if first_event:
|
||||
if first_event.source_field_size == 8:
|
||||
return '3600'
|
||||
return "3600"
|
||||
elif first_event.source_field_size == 32:
|
||||
return 'File32'
|
||||
return "File32"
|
||||
elif first_event.source_field_size == 128:
|
||||
return 'File128'
|
||||
return "File128"
|
||||
else:
|
||||
return 'unknown'
|
||||
return "unknown"
|
||||
else:
|
||||
return 'unknown'
|
||||
return "unknown"
|
||||
|
||||
@property
|
||||
def channels(self) -> ChannelMap:
|
||||
@@ -72,12 +80,12 @@ class EditList:
|
||||
:class:`StmtCorruptRemark`
|
||||
"""
|
||||
for s in self.event_statements:
|
||||
if type(s) is StmtUnrecognized or type(s) in StmtCorruptRemark:
|
||||
if type(s) is StmtUnrecognized or type(s) is StmtCorruptRemark:
|
||||
yield s
|
||||
|
||||
@property
|
||||
def events(self) -> Generator[Event, None, None]:
|
||||
'A generator for all the events in the edit list'
|
||||
"A generator for all the events in the edit list"
|
||||
current_event_num = None
|
||||
event_statements = []
|
||||
for stmt in self.event_statements:
|
||||
|
||||
+64
-33
@@ -1,18 +1,27 @@
|
||||
# pycmx
|
||||
# (c) 2023-2025 Jamie Hardt
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Generator
|
||||
|
||||
from .statements import (StmtFrmc, StmtEvent, StmtClipName, StmtSourceFile,
|
||||
StmtAudioExt, StmtUnrecognized, StmtEffectsName,
|
||||
StmtCdlSop, StmtCdlSat)
|
||||
from .edit import Edit
|
||||
|
||||
from typing import List, Generator, Optional, Tuple, Any
|
||||
from .statements import (
|
||||
StmtAudioExt,
|
||||
StmtCdlSat,
|
||||
StmtCdlSop,
|
||||
StmtClipName,
|
||||
StmtEffectsName,
|
||||
StmtEvent,
|
||||
StmtFrmc,
|
||||
StmtSourceFile,
|
||||
StmtUnrecognized,
|
||||
)
|
||||
|
||||
|
||||
class Event:
|
||||
"""
|
||||
Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same
|
||||
event number. """
|
||||
event number."""
|
||||
|
||||
def __init__(self, statements):
|
||||
self.statements = statements
|
||||
@@ -25,26 +34,41 @@ class Event:
|
||||
return int(self._edit_statements()[0].event)
|
||||
|
||||
@property
|
||||
def edits(self) -> List[Edit]:
|
||||
def edits(self) -> list[Edit]:
|
||||
"""
|
||||
Returns the edits. Most events will have a single edit, a single event
|
||||
will have multiple edits when a dissolve, wipe or key transition needs
|
||||
to be performed.
|
||||
"""
|
||||
|
||||
# FTR this is a totall bonkers way of doing this, I wrote this when
|
||||
# I was still learning Python and I'm sure there's easier ways to do
|
||||
# it. The job is complicated because multiple edits can occur in one
|
||||
# event and then other statements can modify the event in different
|
||||
# ways.
|
||||
|
||||
edits_audio = list(self._statements_with_audio_ext())
|
||||
clip_names = self._clip_name_statements()
|
||||
source_files = self._source_file_statements()
|
||||
|
||||
the_zip: List[List[Any]] = [edits_audio]
|
||||
# We first get the edit events combined with their extra audio
|
||||
# channel statements, if any.
|
||||
|
||||
# The list the_zip contains one element for each initialization
|
||||
# parameter in Edit()
|
||||
the_zip: list[list[Any]] = [edits_audio]
|
||||
|
||||
# If there are two Clip Name statements and two edits, we look for
|
||||
# "FROM" and "TO" clip name lines. Otherwise we just look for on
|
||||
# each per edit.
|
||||
if len(edits_audio) == 2:
|
||||
start_name: Optional[StmtClipName] = None
|
||||
end_name: Optional[StmtClipName] = None
|
||||
start_name: StmtClipName | None = None
|
||||
end_name: StmtClipName | None = None
|
||||
|
||||
for clip_name in clip_names:
|
||||
if clip_name.affect == 'from':
|
||||
if clip_name.affect == "from":
|
||||
start_name = clip_name
|
||||
elif clip_name.affect == 'to':
|
||||
elif clip_name.affect == "to":
|
||||
end_name = clip_name
|
||||
|
||||
the_zip.append([start_name, end_name])
|
||||
@@ -54,6 +78,10 @@ class Event:
|
||||
else:
|
||||
the_zip.append([None] * len(edits_audio))
|
||||
|
||||
# if there's one source file statemnent per clip, we allocate them to
|
||||
# each edit in order. Otherwise if there's only one, we assign the one
|
||||
# to all the edits. If there's no source_file statements, we provide
|
||||
# None.
|
||||
if len(edits_audio) == len(source_files):
|
||||
the_zip.append(source_files)
|
||||
elif len(source_files) == 1:
|
||||
@@ -61,27 +89,31 @@ class Event:
|
||||
else:
|
||||
the_zip.append([None] * len(edits_audio))
|
||||
|
||||
# attach trans name to last event
|
||||
# attach effects name to last event
|
||||
try:
|
||||
trans_statement = self._trans_name_statements()[0]
|
||||
trans_names: List[Optional[Any]] = [None] * (len(edits_audio) - 1)
|
||||
trans_names: list[Any] = [None] * (len(edits_audio) - 1)
|
||||
trans_names.append(trans_statement)
|
||||
the_zip.append(trans_names)
|
||||
except IndexError:
|
||||
the_zip.append([None] * len(edits_audio))
|
||||
return [Edit(edit_statement=e1[0],
|
||||
|
||||
return [
|
||||
Edit(
|
||||
edit_statement=e1[0],
|
||||
audio_ext_statement=e1[1],
|
||||
clip_name_statement=n1,
|
||||
source_file_statement=s1,
|
||||
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)]
|
||||
frmc_statement=self._frmc_statement(),
|
||||
)
|
||||
for (e1, n1, s1, u1) in zip(*the_zip)
|
||||
]
|
||||
|
||||
@property
|
||||
def unrecognized_statements(self) -> Generator[StmtUnrecognized, None,
|
||||
None]:
|
||||
def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]:
|
||||
"""
|
||||
A generator for all the unrecognized statements in the event.
|
||||
"""
|
||||
@@ -89,38 +121,37 @@ class Event:
|
||||
if type(s) is StmtUnrecognized:
|
||||
yield s
|
||||
|
||||
def _trans_name_statements(self) -> List[StmtEffectsName]:
|
||||
def _trans_name_statements(self) -> list[StmtEffectsName]:
|
||||
return [s for s in self.statements if type(s) is StmtEffectsName]
|
||||
|
||||
def _edit_statements(self) -> List[StmtEvent]:
|
||||
def _edit_statements(self) -> list[StmtEvent]:
|
||||
return [s for s in self.statements if type(s) is StmtEvent]
|
||||
|
||||
def _clip_name_statements(self) -> List[StmtClipName]:
|
||||
def _clip_name_statements(self) -> list[StmtClipName]:
|
||||
return [s for s in self.statements if type(s) is StmtClipName]
|
||||
|
||||
def _source_file_statements(self) -> List[StmtSourceFile]:
|
||||
def _source_file_statements(self) -> list[StmtSourceFile]:
|
||||
return [s for s in self.statements if type(s) is StmtSourceFile]
|
||||
|
||||
def _statements_with_audio_ext(self) -> Generator[
|
||||
Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
|
||||
def _statements_with_audio_ext(
|
||||
self,
|
||||
) -> Generator[tuple[StmtEvent, StmtAudioExt | None], None, 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:]):
|
||||
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_sop_statement(self) -> StmtCdlSop | None:
|
||||
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 _asc_sat_statement(self) -> StmtCdlSat | None:
|
||||
return next((s for s in self.statements if type(s) is StmtCdlSat), None)
|
||||
|
||||
def _frmc_statement(self) -> Optional[StmtFrmc]:
|
||||
def _frmc_statement(self) -> StmtFrmc | None:
|
||||
return next((s for s in self.statements if type(s) is StmtFrmc), None)
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
from typing import TextIO
|
||||
|
||||
from .parse_cmx_statements import (parse_cmx3600_statements)
|
||||
from .edit_list import EditList
|
||||
from .parse_cmx_statements import parse_cmx3600_statements
|
||||
|
||||
|
||||
def parse_cmx3600(f: TextIO, tolerant: bool = False) -> EditList:
|
||||
|
||||
@@ -1,37 +1,63 @@
|
||||
# pycmx
|
||||
# (c) 2018-2025 Jamie Hardt
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TextIO, List
|
||||
from typing import TextIO
|
||||
|
||||
from .cdl import AscSopComponents, Rgb
|
||||
|
||||
from .statements import (StmtCdlSat, StmtCdlSop, StmtCorruptRemark, StmtFrmc,
|
||||
StmtRemark, StmtTitle, StmtUnrecognized, StmtFCM,
|
||||
StmtAudioExt, StmtClipName, StmtEffectsName,
|
||||
StmtEvent, StmtSourceFile, StmtSplitEdit)
|
||||
from .statements import (
|
||||
StmtAudioExt,
|
||||
StmtCdlSat,
|
||||
StmtCdlSop,
|
||||
StmtClipName,
|
||||
StmtCorruptRemark,
|
||||
StmtEffectsName,
|
||||
StmtEvent,
|
||||
StmtFCM,
|
||||
StmtFrmc,
|
||||
StmtRemark,
|
||||
StmtSourceFile,
|
||||
StmtSplitEdit,
|
||||
StmtTitle,
|
||||
StmtUnrecognized,
|
||||
)
|
||||
from .util import collimate
|
||||
|
||||
|
||||
def parse_cmx3600_statements(file: TextIO,
|
||||
tolerant: bool = False) -> 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, tolerant)
|
||||
for (line_number, line) in enumerate(lines)]
|
||||
return [
|
||||
_parse_cmx3600_line(line.strip(), line_number, tolerant)
|
||||
for (line_number, line) in enumerate(lines)
|
||||
]
|
||||
|
||||
|
||||
def _edl_column_widths(event_field_length, source_field_length) -> List[int]:
|
||||
return [event_field_length, 2, source_field_length, 1,
|
||||
4, 2, # chans
|
||||
4, 1, # trans
|
||||
3, 1, # trans op
|
||||
11, 1,
|
||||
11, 1,
|
||||
11, 1,
|
||||
11]
|
||||
def _edl_column_widths(event_field_length, source_field_length) -> list[int]:
|
||||
return [
|
||||
event_field_length,
|
||||
2,
|
||||
source_field_length,
|
||||
1,
|
||||
4,
|
||||
2, # chans
|
||||
4,
|
||||
1, # trans
|
||||
3,
|
||||
1, # trans op
|
||||
11,
|
||||
1,
|
||||
11,
|
||||
1,
|
||||
11,
|
||||
1,
|
||||
11,
|
||||
]
|
||||
|
||||
|
||||
# def _edl_m2_column_widths():
|
||||
# return [2, # "M2"
|
||||
@@ -39,8 +65,7 @@ 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,
|
||||
tolerant: bool = False) -> object:
|
||||
def _parse_cmx3600_line(line: str, line_number: int, tolerant: bool = False) -> object:
|
||||
"""
|
||||
Parses a single CMX EDL line.
|
||||
|
||||
@@ -61,7 +86,8 @@ def _parse_cmx3600_line(line: str, line_number: int,
|
||||
|
||||
try:
|
||||
return _parse_columns_for_standard_form(
|
||||
line, event_field_len, source_field_len, line_number)
|
||||
line, event_field_len, source_field_len, line_number
|
||||
)
|
||||
|
||||
except EventFormError:
|
||||
if tolerant:
|
||||
@@ -112,73 +138,88 @@ def _parse_extended_audio_channels(line, line_number):
|
||||
|
||||
def _parse_remark(line, line_number) -> object:
|
||||
if line.startswith("FROM CLIP NAME:"):
|
||||
return StmtClipName(name=line[15:].strip(), affect="from",
|
||||
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)
|
||||
return StmtClipName(
|
||||
name=line[13:].strip(), affect="to", line_number=line_number
|
||||
)
|
||||
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)
|
||||
elif line.startswith("ASC_SOP"):
|
||||
group_patterns: list[str] = re.findall(r'\((.*?)\)', line)
|
||||
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]
|
||||
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]):
|
||||
if len(v) != 3 or any(len(a) != 3 for a in v):
|
||||
return StmtRemark(line, line_number)
|
||||
|
||||
else:
|
||||
try:
|
||||
return StmtCdlSop(line=line,
|
||||
return StmtCdlSop(
|
||||
line=line,
|
||||
cdl_sop=AscSopComponents(
|
||||
slope=Rgb(red=float(v[0][0]),
|
||||
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]))
|
||||
blue=float(v[0][2]),
|
||||
),
|
||||
line_number=line_number)
|
||||
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)
|
||||
return StmtCorruptRemark("ASC_SOP", e, line_number)
|
||||
|
||||
elif line.startswith("ASC_SAT"):
|
||||
value = re.findall(r'(-?\d+(\.\d+)?)', line)
|
||||
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)
|
||||
return StmtCdlSat(value=float(value[0][0]), line_number=line_number)
|
||||
|
||||
except ValueError as e:
|
||||
return StmtCorruptRemark('ASC_SAT', e, line_number)
|
||||
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)
|
||||
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)
|
||||
return StmtCorruptRemark("FRMC", None, line_number)
|
||||
|
||||
else:
|
||||
try:
|
||||
return StmtFrmc(start=int(match.group(1)),
|
||||
return StmtFrmc(
|
||||
start=int(match.group(1)),
|
||||
end=int(match.group(2)),
|
||||
duration=int(match.group(3)),
|
||||
line_number=line_number)
|
||||
line_number=line_number,
|
||||
)
|
||||
except ValueError as e:
|
||||
return StmtCorruptRemark('FRMC', e, line_number)
|
||||
return StmtCorruptRemark("FRMC", e, line_number)
|
||||
|
||||
else:
|
||||
return StmtRemark(text=line, line_number=line_number)
|
||||
@@ -194,14 +235,14 @@ def _parse_split(line: str, line_number):
|
||||
is_video = split_type.startswith("VIDEO")
|
||||
|
||||
split_delay = line[24:35]
|
||||
return StmtSplitEdit(video=is_video, delay=split_delay,
|
||||
line_number=line_number)
|
||||
return StmtSplitEdit(video=is_video, delay=split_delay, line_number=line_number)
|
||||
|
||||
|
||||
# def _parse_motion_memory(line, line_number):
|
||||
# return StmtMotionMemory(source="", fps="")
|
||||
#
|
||||
|
||||
|
||||
class EventFormError(RuntimeError):
|
||||
pass
|
||||
|
||||
@@ -210,9 +251,9 @@ def _parse_unrecognized(line, line_number):
|
||||
return StmtUnrecognized(content=line, line_number=line_number)
|
||||
|
||||
|
||||
def _parse_columns_for_standard_form(line: str, event_field_length: int,
|
||||
source_field_length: int,
|
||||
line_number: int):
|
||||
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)
|
||||
|
||||
@@ -227,7 +268,8 @@ def _parse_columns_for_standard_form(line: str, event_field_length: int,
|
||||
if len(channels) == 0 or len(trans) == 0:
|
||||
raise EventFormError()
|
||||
|
||||
return StmtEvent(event=column_strings[0],
|
||||
return StmtEvent(
|
||||
event=int(column_strings[0]),
|
||||
source=column_strings[2].strip(),
|
||||
channels=channels,
|
||||
trans=trans,
|
||||
@@ -237,25 +279,34 @@ def _parse_columns_for_standard_form(line: str, event_field_length: int,
|
||||
record_in=column_strings[14].strip(),
|
||||
record_out=column_strings[16].strip(),
|
||||
line_number=line_number,
|
||||
source_field_size=source_field_length)
|
||||
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)'
|
||||
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)))
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# pycmx
|
||||
# (c) 2023 Jamie Hardt
|
||||
|
||||
from typing import Optional
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class Transition:
|
||||
@@ -22,7 +22,7 @@ class Transition:
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def kind(self) -> Optional[str]:
|
||||
def kind(self) -> str | None:
|
||||
"""
|
||||
Return the kind of transition: Cut, Wipe, etc.
|
||||
"""
|
||||
@@ -42,17 +42,17 @@ class Transition:
|
||||
@property
|
||||
def cut(self) -> bool:
|
||||
"`True` if this transition is a cut."
|
||||
return self.transition == 'C'
|
||||
return self.transition == "C"
|
||||
|
||||
@property
|
||||
def dissolve(self) -> bool:
|
||||
"`True` if this traansition is a dissolve."
|
||||
return self.transition == 'D'
|
||||
return self.transition == "D"
|
||||
|
||||
@property
|
||||
def wipe(self) -> bool:
|
||||
"`True` if this transition is a wipe."
|
||||
return self.transition.startswith('W')
|
||||
return self.transition.startswith("W")
|
||||
|
||||
@property
|
||||
def effect_duration(self) -> int:
|
||||
@@ -64,7 +64,7 @@ class Transition:
|
||||
return int(self.operand)
|
||||
|
||||
@property
|
||||
def wipe_number(self) -> Optional[int]:
|
||||
def wipe_number(self) -> int | None:
|
||||
"Wipes are identified by a particular number."
|
||||
if self.wipe:
|
||||
return int(self.transition[1:])
|
||||
|
||||
+4
-1
@@ -3,7 +3,10 @@
|
||||
|
||||
# Utility functions
|
||||
|
||||
def collimate(a_string, column_widths):
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def collimate(a_string, column_widths) -> list[str]:
|
||||
"""
|
||||
Split a list-type thing, like a string, into slices that are column_widths
|
||||
length.
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from . import test_parse
|
||||
from . import test_issue_19
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: SIM115
|
||||
from unittest import TestCase
|
||||
|
||||
from pycmx import parse_cmx3600
|
||||
|
||||
+38
-41
@@ -5,64 +5,65 @@ import pycmx
|
||||
|
||||
class TestParse(TestCase):
|
||||
|
||||
files = ["INS4_R1_010417.edl",
|
||||
def setUp(self) -> None:
|
||||
self.files = [
|
||||
"INS4_R1_010417.edl",
|
||||
"INS4_R1_DX_092117.edl",
|
||||
"STP R1 v082517.edl",
|
||||
"ToD_R4_LOCK3.1_030618_Video.edl",
|
||||
"TEST.edl",
|
||||
"test_edl_cdl.edl",
|
||||
"INS4_R1_DX_092117.edl"
|
||||
"INS4_R1_DX_092117.edl",
|
||||
]
|
||||
|
||||
return super().setUp()
|
||||
|
||||
def test_event_counts(self):
|
||||
|
||||
counts = [287, 466, 250, 376, 120, 3, 466]
|
||||
|
||||
for fn, count in zip(type(self).files, counts):
|
||||
with open("tests/edls/" + fn, 'r') as f:
|
||||
for fn, count in zip(self.files, counts):
|
||||
with open("tests/edls/" + fn, "r") as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
actual = len(list(edl.events))
|
||||
self.assertTrue(actual == count,
|
||||
"expected %i in file %s but found %i"
|
||||
% (count, fn, actual))
|
||||
self.assertTrue(
|
||||
actual == count,
|
||||
f"expected {count} in file {fn} but found {actual}"
|
||||
)
|
||||
|
||||
def test_list_sanity(self):
|
||||
for fn in type(self).files:
|
||||
with open("tests/edls/" + fn, 'r') as f:
|
||||
for fn in self.files:
|
||||
with open("tests/edls/" + fn, "r") as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
self.assertTrue(type(edl.title) is str)
|
||||
self.assertTrue(len(edl.title) > 0)
|
||||
|
||||
def test_event_sanity(self):
|
||||
for fn in type(self).files:
|
||||
for fn in self.files:
|
||||
path = "tests/edls/" + fn
|
||||
with open(path, 'r') as f:
|
||||
with open(path, "r") as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
for index, event in enumerate(edl.events):
|
||||
self.assertTrue(len(event.edits) > 0,
|
||||
f"Failed for {path}")
|
||||
self.assertEqual(event.number, index + 1,
|
||||
f"Failed for {path}")
|
||||
self.assertTrue(len(event.edits) > 0, f"Failed for {path}")
|
||||
self.assertEqual(event.number, index + 1, f"Failed for {path}")
|
||||
|
||||
def test_events(self):
|
||||
with open("tests/edls/TEST.edl", 'r') as f:
|
||||
with open("tests/edls/TEST.edl", "r") as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
events = list(edl.events)
|
||||
|
||||
self.assertEqual(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(
|
||||
events[0].edits[0].source_file, "OY_HEAD_LEADER.MOV")
|
||||
self.assertEqual(events[0].edits[0].source_file, "OY_HEAD_LEADER.MOV")
|
||||
self.assertEqual(events[0].edits[0].source_in, "00:00:00:00")
|
||||
self.assertEqual(events[0].edits[0].source_out, "00:00:00:00")
|
||||
self.assertEqual(events[0].edits[0].record_in, "01:00:00:00")
|
||||
self.assertEqual(events[0].edits[0].record_out, "01:00:08:00")
|
||||
self.assertTrue(
|
||||
events[0].edits[0].transition.kind == pycmx.Transition.Cut)
|
||||
self.assertTrue(events[0].edits[0].transition.kind == pycmx.Transition.Cut)
|
||||
|
||||
def test_channel_map(self):
|
||||
with open("tests/edls/TEST.edl", 'r') as f:
|
||||
with open("tests/edls/TEST.edl", "r") as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
events = list(edl.events)
|
||||
self.assertFalse(events[0].edits[0].channels.video)
|
||||
@@ -72,7 +73,7 @@ class TestParse(TestCase):
|
||||
self.assertTrue(events[2].edits[0].channels.audio)
|
||||
|
||||
def test_multi_edit_events(self):
|
||||
with open("tests/edls/TEST.edl", 'r') as f:
|
||||
with open("tests/edls/TEST.edl", "r") as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
events = list(edl.events)
|
||||
|
||||
@@ -80,25 +81,24 @@ class TestParse(TestCase):
|
||||
self.assertEqual(len(events[42].edits), 2)
|
||||
|
||||
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[0].clip_name, "TC R1 V1.2 TEMP1 FX ST.WAV"
|
||||
)
|
||||
self.assertEqual(events[42].edits[0].source_in, "00:00:00:00")
|
||||
self.assertEqual(events[42].edits[0].source_out, "00:00:00:00")
|
||||
self.assertEqual(events[42].edits[0].record_in, "01:08:56:09")
|
||||
self.assertEqual(events[42].edits[0].record_out, "01:08:56:09")
|
||||
self.assertTrue(
|
||||
events[42].edits[0].transition.kind == pycmx.Transition.Cut)
|
||||
self.assertTrue(events[42].edits[0].transition.kind == pycmx.Transition.Cut)
|
||||
|
||||
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")
|
||||
self.assertEqual(events[42].edits[1].clip_name, "TC R1 V6 TEMP2 ST FX.WAV")
|
||||
self.assertEqual(events[42].edits[1].source_in, "00:00:00:00")
|
||||
self.assertEqual(events[42].edits[1].source_out, "00:00:00:00")
|
||||
self.assertEqual(events[42].edits[1].record_in, "01:08:56:09")
|
||||
self.assertEqual(events[42].edits[1].record_out, "01:08:56:11")
|
||||
self.assertTrue(
|
||||
events[42].edits[1].transition.kind ==
|
||||
pycmx.Transition.Dissolve)
|
||||
events[42].edits[1].transition.kind == pycmx.Transition.Dissolve
|
||||
)
|
||||
|
||||
def test_line_numbers(self):
|
||||
with open("tests/edls/ToD_R4_LOCK3.1_030618_Video.edl") as f:
|
||||
@@ -113,12 +113,10 @@ class TestParse(TestCase):
|
||||
with open("tests/edls/test_25.edl", "r") as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
events = list(edl.events)
|
||||
self.assertEqual(
|
||||
events[4].edits[1].transition.name, "CROSS DISSOLVE")
|
||||
self.assertEqual(events[4].edits[1].transition.name, "CROSS DISSOLVE")
|
||||
|
||||
def test_adobe_wide(self):
|
||||
with open("tests/edls/adobe_dai109_test.txt", 'r',
|
||||
encoding='ISO-8859-1') as f:
|
||||
with open("tests/edls/adobe_dai109_test.txt", "r", encoding="ISO-8859-1") as f:
|
||||
edl = pycmx.parse_cmx3600(f)
|
||||
events = list(edl.events)
|
||||
|
||||
@@ -131,12 +129,11 @@ class TestParse(TestCase):
|
||||
for event in edl.events:
|
||||
if event.number == 42:
|
||||
self.assertEqual(len(event.edits), 1)
|
||||
self.assertEqual(event.edits[0].source,
|
||||
"M018C0005_240925_1F4L13")
|
||||
self.assertEqual(event.edits[0].transition.kind,
|
||||
pycmx.Transition.Cut)
|
||||
self.assertEqual(event.edits[0].source_in,
|
||||
"18:44:20:12")
|
||||
self.assertEqual(event.edits[0].source, "M018C0005_240925_1F4L13")
|
||||
self.assertEqual(
|
||||
event.edits[0].transition.kind, 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:
|
||||
@@ -152,7 +149,7 @@ class TestParse(TestCase):
|
||||
sat = event.edits[0].asc_sat
|
||||
self.assertIsNotNone(sat)
|
||||
assert sat
|
||||
self.assertEqual(sat, float('0.9640'))
|
||||
self.assertEqual(sat, float("0.9640"))
|
||||
|
||||
def test_frmc(self):
|
||||
with open("tests/edls/cdl_frmc_example01.edl", "r") as f:
|
||||
|
||||
Reference in New Issue
Block a user