mirror of
https://github.com/iluvcapra/pycmx.git
synced 2026-10-02 09:47:23 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f0a9db555 | |||
| d61adad4c3 | |||
| 03b4119ccf | |||
| 5e560dfa0e | |||
| daedabd7cc | |||
| 9fd5d36611 | |||
| e8556958b5 | |||
| 98e21e7a09 | |||
| 694b99d081 | |||
| 2e4a0439c1 | |||
| 688402d195 | |||
| 4e81810584 | |||
| faf2596a57 | |||
| 1e9fbe339c | |||
| a8d00470d4 | |||
| fe1e59e731 | |||
| ec8a08074d | |||
| ef683a7683 | |||
| d778f64230 | |||
| 1b0ccd4ef7 | |||
| 498d5c8fea | |||
| af5d937aeb |
@@ -1,5 +0,0 @@
|
|||||||
[flake8]
|
|
||||||
per-file-ignores =
|
|
||||||
src/pycmx/__init__.py: F401
|
|
||||||
tests/__init__.py: F401
|
|
||||||
|
|
||||||
@@ -27,11 +27,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
python -m pip install -e .[dev]
|
python -m pip install -e .[dev]
|
||||||
- name: Lint with flake8
|
- name: Lint with ruff
|
||||||
run: |
|
run: |
|
||||||
# stop the build if there are Python syntax errors or undefined names
|
ruff check src/
|
||||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
||||||
flake8 . --count --max-line-length=79 --statistics
|
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
pytest
|
pytest
|
||||||
|
|||||||
+2
-2
@@ -7,13 +7,13 @@ version: 2
|
|||||||
|
|
||||||
# Set the version of Python and other tools you might need
|
# Set the version of Python and other tools you might need
|
||||||
build:
|
build:
|
||||||
os: ubuntu-20.04
|
os: ubuntu-lts-latest
|
||||||
tools:
|
tools:
|
||||||
python: "3.13"
|
python: "3.13"
|
||||||
jobs:
|
jobs:
|
||||||
install:
|
install:
|
||||||
- pip install --upgrade pip
|
- pip install --upgrade pip
|
||||||
- pip install --group 'doc'
|
- pip install --group 'doc' -e .
|
||||||
|
|
||||||
# Build documentation in the docs/ directory with Sphinx
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
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
|
If you have EDLs you are having trouble working with becuase of unusual formatting, please send me a copy! Contact us
|
||||||
through [Github].
|
through [Github].
|
||||||
|
|
||||||
|
|
||||||
[github]: https://github.com/iluvcapra/pycmx
|
[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.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
|
|||||||
### Opening and Parsing EDL Files
|
### Opening and Parsing EDL Files
|
||||||
```
|
```
|
||||||
>>> import pycmx
|
>>> import pycmx
|
||||||
>>> with open("tests/edls/TEST.edl") as f
|
>>> with open("tests/edls/TEST.edl") as f:
|
||||||
... edl = pycmx.parse_cmx3600(f)
|
... edl = pycmx.parse_cmx3600(f)
|
||||||
...
|
...
|
||||||
>>> edl.title
|
>>> edl.title
|
||||||
|
|||||||
+56
-35
@@ -1,12 +1,11 @@
|
|||||||
import pycmx
|
|
||||||
import re
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import logging
|
import pycmx
|
||||||
|
|
||||||
|
FORMAT = "%(asctime)-15s %(message)s"
|
||||||
FORMAT = '%(asctime)-15s %(message)s'
|
|
||||||
logging.basicConfig(format=FORMAT)
|
logging.basicConfig(format=FORMAT)
|
||||||
|
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ def all_video_edits(edl):
|
|||||||
|
|
||||||
|
|
||||||
def get_scene_name(edit, pattern):
|
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:
|
if edit.clip_name is None:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
@@ -37,16 +36,15 @@ def output_cmx(outfile, out_list):
|
|||||||
outfile.write("FCM: NON-DROP FRAME\r\n")
|
outfile.write("FCM: NON-DROP FRAME\r\n")
|
||||||
|
|
||||||
for i, o in enumerate(out_list):
|
for i, o in enumerate(out_list):
|
||||||
line = '%03i AX V C ' % (i)
|
line = "{i: 03d} AX V C "
|
||||||
line += '00:00:00:00 00:00:00:00 %s %s\r\n' % (o['start'], o['end'])
|
line += f"00:00:00:00 00:00:00:00 {o['start']} {o['end']}\r\n"
|
||||||
outfile.write(line)
|
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):
|
def output_cols(outfile, out_list):
|
||||||
for o in out_list:
|
for o in out_list:
|
||||||
outfile.write("%-12s\t%-12s\t%s\n" %
|
outfile.write(f"{o['start']:>12}\t{o['end']:>12}\t{o['scene']}")
|
||||||
(o['start'], o['end'], o['scene']))
|
|
||||||
|
|
||||||
|
|
||||||
def scene_list(infile, outfile, out_format, pattern):
|
def scene_list(infile, outfile, out_format, pattern):
|
||||||
@@ -68,15 +66,17 @@ def scene_list(infile, outfile, out_format, pattern):
|
|||||||
|
|
||||||
out_list = []
|
out_list = []
|
||||||
for group in grouped_edits:
|
for group in grouped_edits:
|
||||||
out_list.append({
|
out_list.append(
|
||||||
'start': group[0].record_in,
|
{
|
||||||
'end': group[-1].record_out,
|
"start": group[0].record_in,
|
||||||
'scene': get_scene_name(group[0], pattern)}
|
"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)
|
output_cmx(outfile, out_list)
|
||||||
if out_format == 'cols':
|
if out_format == "cols":
|
||||||
output_cols(outfile, out_list)
|
output_cols(outfile, out_list)
|
||||||
else:
|
else:
|
||||||
log.warning(f"Format {out_format} unrecognized. Will use cmx.\n")
|
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():
|
def scene_list_cli():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Read video events from an input CMX EDL and output '
|
description="Read video events from an input CMX EDL and output "
|
||||||
'events merged into scenes.')
|
"events merged into scenes."
|
||||||
parser.add_argument('-o', '--outfile', default=sys.stdout,
|
)
|
||||||
type=argparse.FileType('w'),
|
parser.add_argument(
|
||||||
help='Output file. Default is stdout.')
|
"-o",
|
||||||
parser.add_argument('-f', '--format', default='cmx', type=str,
|
"--outfile",
|
||||||
help='Output format. Options are cols and cmx, cmx '
|
default=sys.stdout,
|
||||||
'is the default.')
|
type=argparse.FileType("w"),
|
||||||
parser.add_argument('-p', '--pattern', default='V?([A-Z]*[0-9]+)',
|
help="Output file. Default is stdout.",
|
||||||
help='RE pattern for extracting scene name from clip '
|
)
|
||||||
'name. The default is "V?([A-Z]*[0-9]+)". ' +
|
parser.add_argument(
|
||||||
'This pattern will be matched case-insensitively.')
|
"-f",
|
||||||
parser.add_argument('input_edl', default=sys.stdin,
|
"--format",
|
||||||
type=argparse.FileType('r'), nargs='?',
|
default="cmx",
|
||||||
help='Input file. Default is stdin.')
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
infile = args.input_edl
|
infile = args.input_edl
|
||||||
|
|
||||||
scene_list(infile=infile, outfile=args.outfile,
|
scene_list(
|
||||||
out_format=args.format, pattern=args.pattern)
|
infile=infile,
|
||||||
|
outfile=args.outfile,
|
||||||
|
out_format=args.format,
|
||||||
|
pattern=args.pattern,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
scene_list_cli()
|
scene_list_cli()
|
||||||
|
|||||||
+30
-31
@@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Configuration file for the Sphinx documentation builder.
|
# Configuration file for the Sphinx documentation builder.
|
||||||
#
|
#
|
||||||
@@ -15,14 +14,15 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
|
||||||
|
sys.path.insert(0, os.path.abspath("../.."))
|
||||||
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = u'pycmx'
|
project = "pycmx"
|
||||||
copyright = u'(c) 2018-2025, Jamie Hardt'
|
copyright = "(c) 2018-2025, Jamie Hardt"
|
||||||
author = u'Jamie Hardt'
|
author = "Jamie Hardt"
|
||||||
|
|
||||||
release = importlib.metadata.version("pycmx")
|
release = importlib.metadata.version("pycmx")
|
||||||
version = release
|
version = release
|
||||||
@@ -40,31 +40,31 @@ version = release
|
|||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
"sphinx.ext.autodoc",
|
||||||
'sphinx.ext.todo',
|
"sphinx.ext.todo",
|
||||||
'sphinx.ext.coverage',
|
"sphinx.ext.coverage",
|
||||||
'sphinx.ext.viewcode',
|
"sphinx.ext.viewcode",
|
||||||
'sphinx.ext.githubpages',
|
"sphinx.ext.githubpages",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ["_templates"]
|
||||||
|
|
||||||
# The suffix(es) of source filenames.
|
# The suffix(es) of source filenames.
|
||||||
# You can specify multiple suffix as a list of string:
|
# You can specify multiple suffix as a list of string:
|
||||||
#
|
#
|
||||||
# source_suffix = ['.rst', '.md']
|
# source_suffix = ['.rst', '.md']
|
||||||
source_suffix = '.rst'
|
source_suffix = ".rst"
|
||||||
|
|
||||||
# The master toctree document.
|
# The master toctree document.
|
||||||
master_doc = 'index'
|
master_doc = "index"
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
#
|
#
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
# Usually you set "language" from the command line for these cases.
|
# 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
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# 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
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# 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
|
# 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
|
# further. For a list of options available for each theme, see the
|
||||||
# documentation.
|
# 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,
|
# 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,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# 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
|
# Custom sidebar templates, must be a dictionary that maps document names
|
||||||
# to template names.
|
# to template names.
|
||||||
@@ -107,7 +107,7 @@ html_static_path = ['_static']
|
|||||||
# -- Options for HTMLHelp output ---------------------------------------------
|
# -- Options for HTMLHelp output ---------------------------------------------
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = 'pycmxdoc'
|
htmlhelp_basename = "pycmxdoc"
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output ------------------------------------------------
|
# -- Options for LaTeX output ------------------------------------------------
|
||||||
@@ -116,15 +116,12 @@ latex_elements = {
|
|||||||
# The paper size ('letterpaper' or 'a4paper').
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
#
|
#
|
||||||
# 'papersize': 'letterpaper',
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
#
|
#
|
||||||
# 'pointsize': '10pt',
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
# Additional stuff for the LaTeX preamble.
|
||||||
#
|
#
|
||||||
# 'preamble': '',
|
# 'preamble': '',
|
||||||
|
|
||||||
# Latex figure (float) alignment
|
# Latex figure (float) alignment
|
||||||
#
|
#
|
||||||
# 'figure_align': 'htbp',
|
# 'figure_align': 'htbp',
|
||||||
@@ -134,8 +131,7 @@ latex_elements = {
|
|||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
(master_doc, 'pycmx.tex', u'pycmx Documentation',
|
(master_doc, "pycmx.tex", "pycmx Documentation", "Jamie Hardt", "manual"),
|
||||||
u'Jamie Hardt', 'manual'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -143,10 +139,7 @@ latex_documents = [
|
|||||||
|
|
||||||
# One entry per manual page. List of tuples
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [(master_doc, "pycmx", "pycmx Documentation", [author], "3p")]
|
||||||
(master_doc, 'pycmx', u'pycmx Documentation',
|
|
||||||
[author], "3p")
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Texinfo output ----------------------------------------------
|
# -- Options for Texinfo output ----------------------------------------------
|
||||||
@@ -155,9 +148,15 @@ man_pages = [
|
|||||||
# (source start file, target name, title, author,
|
# (source start file, target name, title, author,
|
||||||
# dir menu entry, description, category)
|
# dir menu entry, description, category)
|
||||||
texinfo_documents = [
|
texinfo_documents = [
|
||||||
(master_doc, 'pycmx', u'pycmx Documentation',
|
(
|
||||||
author, 'pycmx', 'One line description of project.',
|
master_doc,
|
||||||
'Miscellaneous'),
|
"pycmx",
|
||||||
|
"pycmx Documentation",
|
||||||
|
author,
|
||||||
|
"pycmx",
|
||||||
|
"One line description of project.",
|
||||||
|
"Miscellaneous",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -176,7 +175,7 @@ epub_title = project
|
|||||||
# epub_uid = ''
|
# epub_uid = ''
|
||||||
|
|
||||||
# A list of files that should not be packed into the epub file.
|
# 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 -------------------------------------------------
|
# -- 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
|
read. Event number field and source name field sizes are determined
|
||||||
dynamically for each statement for a high level of compliance at the expense
|
dynamically for each statement for a high level of compliance at the expense
|
||||||
of strictness.
|
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.
|
use non-standard widths.
|
||||||
* Preserves relationship between events and individual edits/clips.
|
* Preserves relationship between events and individual edits/clips.
|
||||||
* Remark or comment fields with common recognized forms are read and
|
* Remark or comment fields with common recognized forms are read and
|
||||||
|
|||||||
+16
-18
@@ -1,8 +1,8 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "pycmx"
|
name = "pycmx"
|
||||||
version = "1.5.0"
|
version = "1.5.1"
|
||||||
description = "Python CMX 3600 Edit Decision List Parser"
|
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"]
|
license-files = ["LICENSE"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = [
|
keywords = [
|
||||||
@@ -13,7 +13,6 @@ keywords = [
|
|||||||
requires-python = '>3.8'
|
requires-python = '>3.8'
|
||||||
classifiers = [
|
classifiers = [
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
'License :: OSI Approved :: MIT License',
|
|
||||||
'Topic :: Multimedia',
|
'Topic :: Multimedia',
|
||||||
'Topic :: Multimedia :: Video',
|
'Topic :: Multimedia :: Video',
|
||||||
'Topic :: Text Processing',
|
'Topic :: Text Processing',
|
||||||
@@ -29,11 +28,11 @@ classifiers = [
|
|||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
doc = [
|
doc = [
|
||||||
'sphinx >= 5.3.0',
|
'sphinx >= 5.3.0',
|
||||||
'sphinx_rtd_theme >= 1.1.1'
|
'sphinx_rtd_theme >= 1.1.1',
|
||||||
]
|
]
|
||||||
dev = [
|
dev = [
|
||||||
'flake8',
|
'pytest',
|
||||||
'pytest'
|
'ruff>=0.16.8'
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -43,24 +42,23 @@ Repository = "https://github.com/iluvcapra/pycmx.git"
|
|||||||
Tracker = "https://github.com/iluvcapra/pycmx/issues"
|
Tracker = "https://github.com/iluvcapra/pycmx/issues"
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
dev = ['ruff', 'pytest']
|
||||||
doc = ['sphinx', 'sphinx_rtd_theme']
|
doc = ['sphinx', 'sphinx_rtd_theme']
|
||||||
|
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
typeCheckingMode = "basic"
|
typeCheckingMode = "basic"
|
||||||
|
|
||||||
[tool.pylint]
|
[tool.ruff]
|
||||||
max-line-length = 88
|
line-length = 88
|
||||||
disable = [
|
indent-width = 4
|
||||||
"C0103", # (invalid-name)
|
|
||||||
"C0114", # (missing-module-docstring)
|
[tool.ruff.lint]
|
||||||
"C0115", # (missing-class-docstring)
|
# select = ["E", "F", "W"]
|
||||||
"C0116", # (missing-function-docstring)
|
|
||||||
"R0903", # (too-few-public-methods)
|
[tool.ruff.format]
|
||||||
"R0913", # (too-many-arguments)
|
docstring-code-line-length = 88
|
||||||
"W0105", # (pointless-string-statement)
|
|
||||||
]
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["uv_build>=0.9.18,<0.10.0"]
|
requires = ["uv_build>=0.9.18,<0.12"]
|
||||||
build-backend = "uv_build"
|
build-backend = "uv_build"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
"""
|
||||||
pycmx is a parser for CMX 3600-style EDLs.
|
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.
|
distribution.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from .edit import Edit
|
||||||
|
from .event import Event
|
||||||
from .parse_cmx_events import parse_cmx3600
|
from .parse_cmx_events import parse_cmx3600
|
||||||
from .transition import Transition
|
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 dataclasses import dataclass
|
||||||
from typing import Generic, NamedTuple, TypeVar
|
from typing import Generic, NamedTuple, TypeVar
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
+21
-21
@@ -1,8 +1,10 @@
|
|||||||
# pycmx
|
# pycmx
|
||||||
# (c) 2018-2025 Jamie Hardt
|
# (c) 2018-2025 Jamie Hardt
|
||||||
|
|
||||||
from re import (compile, match)
|
from __future__ import annotations
|
||||||
from typing import Dict, Tuple, Generator
|
|
||||||
|
from re import compile, match
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
|
|
||||||
class ChannelMap:
|
class ChannelMap:
|
||||||
@@ -10,35 +12,33 @@ class ChannelMap:
|
|||||||
Represents a set of all the channels to which an event applies.
|
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):
|
||||||
"V": (True, False, False),
|
self._audio_channel_set = audio_channels or set()
|
||||||
"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)
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, v=False, audio_channels=set()):
|
|
||||||
self._audio_channel_set = audio_channels
|
|
||||||
self.v = v
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def video(self) -> bool:
|
def video(self) -> bool:
|
||||||
'True if video is included'
|
"True if video is included"
|
||||||
return self.v
|
return self.v
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def audio(self) -> bool:
|
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
|
return len(self._audio_channel_set) > 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def channels(self) -> Generator[int, None, None]:
|
def channels(self) -> Generator[int, None, None]:
|
||||||
'A generator for each audio channel'
|
"A generator for each audio channel"
|
||||||
for c in self._audio_channel_set:
|
yield from self._audio_channel_set
|
||||||
yield c
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def a1(self) -> bool:
|
def a1(self) -> bool:
|
||||||
@@ -78,7 +78,7 @@ class ChannelMap:
|
|||||||
|
|
||||||
def get_audio_channel(self, chan_num) -> bool:
|
def get_audio_channel(self, chan_num) -> bool:
|
||||||
"""True if chan_num is included"""
|
"""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):
|
def set_audio_channel(self, chan_num, enabled: bool):
|
||||||
"""If enabled is true, chan_num will be included"""
|
"""If enabled is true, chan_num will be included"""
|
||||||
@@ -88,7 +88,7 @@ class ChannelMap:
|
|||||||
self._audio_channel_set.remove(chan_num)
|
self._audio_channel_set.remove(chan_num)
|
||||||
|
|
||||||
def _append_event(self, event_str):
|
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:
|
if event_str in self._chan_map:
|
||||||
channels = self._chan_map[event_str]
|
channels = self._chan_map[event_str]
|
||||||
self.v = channels[0]
|
self.v = channels[0]
|
||||||
|
|||||||
+31
-34
@@ -1,21 +1,21 @@
|
|||||||
# pycmx
|
# pycmx
|
||||||
# (c) 2018-2025 Jamie Hardt
|
# (c) 2018-2025 Jamie Hardt
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from .cdl import AscSopComponents, FramecountTriple
|
from .cdl import AscSopComponents, FramecountTriple
|
||||||
|
from .channel_map import ChannelMap
|
||||||
from .statements import (
|
from .statements import (
|
||||||
|
StmtAudioExt,
|
||||||
StmtCdlSat,
|
StmtCdlSat,
|
||||||
StmtCdlSop,
|
StmtCdlSop,
|
||||||
StmtFrmc,
|
|
||||||
StmtEvent,
|
|
||||||
StmtAudioExt,
|
|
||||||
StmtClipName,
|
StmtClipName,
|
||||||
StmtSourceFile,
|
|
||||||
StmtEffectsName,
|
StmtEffectsName,
|
||||||
|
StmtEvent,
|
||||||
|
StmtFrmc,
|
||||||
|
StmtSourceFile,
|
||||||
)
|
)
|
||||||
from .transition import Transition
|
from .transition import Transition
|
||||||
from .channel_map import ChannelMap
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
class Edit:
|
class Edit:
|
||||||
@@ -27,32 +27,29 @@ class Edit:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
edit_statement: StmtEvent,
|
edit_statement: StmtEvent,
|
||||||
audio_ext_statement: Optional[StmtAudioExt],
|
audio_ext_statement: StmtAudioExt | None,
|
||||||
clip_name_statement: Optional[StmtClipName],
|
clip_name_statement: StmtClipName | None,
|
||||||
source_file_statement: Optional[StmtSourceFile],
|
source_file_statement: StmtSourceFile | None,
|
||||||
trans_name_statement: Optional[StmtEffectsName] = None,
|
trans_name_statement: StmtEffectsName | None = None,
|
||||||
asc_sop_statement: Optional[StmtCdlSop] = None,
|
asc_sop_statement: StmtCdlSop | None = None,
|
||||||
asc_sat_statement: Optional[StmtCdlSat] = None,
|
asc_sat_statement: StmtCdlSat | None = None,
|
||||||
frmc_statement: Optional[StmtFrmc] = None,
|
frmc_statement: StmtFrmc | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Assigning types for the attributes explicitly
|
|
||||||
self._edit_statement: StmtEvent = edit_statement
|
self._edit_statement: StmtEvent = edit_statement
|
||||||
self._audio_ext: Optional[StmtAudioExt] = audio_ext_statement
|
self._audio_ext: StmtAudioExt | None = audio_ext_statement
|
||||||
self._clip_name_statement: Optional[StmtClipName] = clip_name_statement
|
self._clip_name_statement: StmtClipName | None = clip_name_statement
|
||||||
self._source_file_statement: Optional[StmtSourceFile] = \
|
self._source_file_statement: StmtSourceFile | None = source_file_statement
|
||||||
source_file_statement
|
self._trans_name_statement: StmtEffectsName | None = trans_name_statement
|
||||||
self._trans_name_statement: Optional[StmtEffectsName] = \
|
self._asc_sop_statement: StmtCdlSop | None = asc_sop_statement
|
||||||
trans_name_statement
|
self._asc_sat_statement: StmtCdlSat | None = asc_sat_statement
|
||||||
self._asc_sop_statement: Optional[StmtCdlSop] = asc_sop_statement
|
self._frmc_statement: StmtFrmc | None = frmc_statement
|
||||||
self._asc_sat_statement: Optional[StmtCdlSat] = asc_sat_statement
|
|
||||||
self._frmc_statement: Optional[StmtFrmc] = frmc_statement
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def line_number(self) -> int:
|
def line_number(self) -> int:
|
||||||
"""
|
"""
|
||||||
Get the line number for the "standard form" statement associated with
|
Get the line number for the "standard form" statement associated with
|
||||||
this edit. Line numbers a zero-indexed, such that the
|
this edit. Line numbers a zero-indexed, such that the "TITLE:" record
|
||||||
"TITLE:" record is line zero.
|
is line zero.
|
||||||
"""
|
"""
|
||||||
return self._edit_statement.line_number
|
return self._edit_statement.line_number
|
||||||
|
|
||||||
@@ -139,7 +136,7 @@ class Edit:
|
|||||||
return self.source == "AX"
|
return self.source == "AX"
|
||||||
|
|
||||||
@property
|
@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
|
Get the source file, as attested by a "* SOURCE FILE" remark on the
|
||||||
EDL. This will return None if the information is not present.
|
EDL. This will return None if the information is not present.
|
||||||
@@ -150,7 +147,7 @@ class Edit:
|
|||||||
return self._source_file_statement.filename
|
return self._source_file_statement.filename
|
||||||
|
|
||||||
@property
|
@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
|
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
|
NAME" remark on the EDL. This will return None if the information is
|
||||||
@@ -162,7 +159,7 @@ class Edit:
|
|||||||
return self._clip_name_statement.name
|
return self._clip_name_statement.name
|
||||||
|
|
||||||
@property
|
@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,
|
Get ASC CDL Slope-Offset-Power color transfer function for the edit,
|
||||||
if present. The ASC SOP is a transfer function of the form:
|
if present. The ASC SOP is a transfer function of the form:
|
||||||
@@ -178,9 +175,9 @@ class Edit:
|
|||||||
return self._asc_sop_statement.cdl_sop
|
return self._asc_sop_statement.cdl_sop
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def asc_sop_raw(self) -> Optional[str]:
|
def asc_sop_raw(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
ASC CDL Slope-Offset-Power statement raw line
|
ASC CDL Slope-Offset-Power statement raw line.
|
||||||
"""
|
"""
|
||||||
if self._asc_sop_statement is None:
|
if self._asc_sop_statement is None:
|
||||||
return None
|
return None
|
||||||
@@ -188,9 +185,9 @@ class Edit:
|
|||||||
return self._asc_sop_statement.line
|
return self._asc_sop_statement.line
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def asc_sat(self) -> Optional[float]:
|
def asc_sat(self) -> float | None:
|
||||||
"""
|
"""
|
||||||
Get ASC CDL saturation value for clip, if present
|
Get ASC CDL saturation value for clip, if present.
|
||||||
"""
|
"""
|
||||||
if self._asc_sat_statement is None:
|
if self._asc_sat_statement is None:
|
||||||
return None
|
return None
|
||||||
@@ -198,7 +195,7 @@ class Edit:
|
|||||||
return self._asc_sat_statement.value
|
return self._asc_sat_statement.value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def framecounts(self) -> Optional[FramecountTriple]:
|
def framecounts(self) -> FramecountTriple | None:
|
||||||
"""
|
"""
|
||||||
Get frame count offset data, if it exists. If an FRMC statement exists
|
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
|
in the EDL for the event it will give an integer frame count for the
|
||||||
|
|||||||
+21
-13
@@ -1,18 +1,25 @@
|
|||||||
# pycmx
|
# pycmx
|
||||||
# (c) 2018-2025 Jamie Hardt
|
# (c) 2018-2025 Jamie Hardt
|
||||||
|
|
||||||
from .statements import (StmtCorruptRemark, StmtTitle, StmtEvent,
|
from __future__ import annotations
|
||||||
StmtUnrecognized, StmtSourceUMID)
|
|
||||||
from .event import Event
|
|
||||||
from .channel_map import ChannelMap
|
|
||||||
|
|
||||||
from typing import Any, Generator
|
from typing import Any, Generator
|
||||||
|
|
||||||
|
from .channel_map import ChannelMap
|
||||||
|
from .event import Event
|
||||||
|
from .statements import (
|
||||||
|
StmtCorruptRemark,
|
||||||
|
StmtEvent,
|
||||||
|
StmtSourceUMID,
|
||||||
|
StmtTitle,
|
||||||
|
StmtUnrecognized,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EditList:
|
class EditList:
|
||||||
"""
|
"""
|
||||||
Represents an entire edit decision list as returned by
|
Represents an entire edit decision list as returned by
|
||||||
:func:`~pycmx.parse_cmx3600()`.
|
:func:`~pycmx.parse_cmx_events.parse_cmx3600()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, statements: list):
|
def __init__(self, statements: list):
|
||||||
@@ -28,19 +35,20 @@ class EditList:
|
|||||||
Adobe EDLs with more than 999 events will be reported as "3600".
|
Adobe EDLs with more than 999 events will be reported as "3600".
|
||||||
"""
|
"""
|
||||||
first_event = next(
|
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:
|
||||||
if first_event.source_field_size == 8:
|
if first_event.source_field_size == 8:
|
||||||
return '3600'
|
return "3600"
|
||||||
elif first_event.source_field_size == 32:
|
elif first_event.source_field_size == 32:
|
||||||
return 'File32'
|
return "File32"
|
||||||
elif first_event.source_field_size == 128:
|
elif first_event.source_field_size == 128:
|
||||||
return 'File128'
|
return "File128"
|
||||||
else:
|
else:
|
||||||
return 'unknown'
|
return "unknown"
|
||||||
else:
|
else:
|
||||||
return 'unknown'
|
return "unknown"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def channels(self) -> ChannelMap:
|
def channels(self) -> ChannelMap:
|
||||||
@@ -72,12 +80,12 @@ class EditList:
|
|||||||
:class:`StmtCorruptRemark`
|
:class:`StmtCorruptRemark`
|
||||||
"""
|
"""
|
||||||
for s in self.event_statements:
|
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
|
yield s
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def events(self) -> Generator[Event, None, None]:
|
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
|
current_event_num = None
|
||||||
event_statements = []
|
event_statements = []
|
||||||
for stmt in self.event_statements:
|
for stmt in self.event_statements:
|
||||||
|
|||||||
+70
-39
@@ -1,18 +1,27 @@
|
|||||||
# pycmx
|
# pycmx
|
||||||
# (c) 2023-2025 Jamie Hardt
|
# (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 .edit import Edit
|
||||||
|
from .statements import (
|
||||||
from typing import List, Generator, Optional, Tuple, Any
|
StmtAudioExt,
|
||||||
|
StmtCdlSat,
|
||||||
|
StmtCdlSop,
|
||||||
|
StmtClipName,
|
||||||
|
StmtEffectsName,
|
||||||
|
StmtEvent,
|
||||||
|
StmtFrmc,
|
||||||
|
StmtSourceFile,
|
||||||
|
StmtUnrecognized,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Event:
|
class Event:
|
||||||
"""
|
"""
|
||||||
Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same
|
Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same
|
||||||
event number. """
|
event number."""
|
||||||
|
|
||||||
def __init__(self, statements):
|
def __init__(self, statements):
|
||||||
self.statements = statements
|
self.statements = statements
|
||||||
@@ -25,26 +34,41 @@ class Event:
|
|||||||
return int(self._edit_statements()[0].event)
|
return int(self._edit_statements()[0].event)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def edits(self) -> List[Edit]:
|
def edits(self) -> list[Edit]:
|
||||||
"""
|
"""
|
||||||
Returns the edits. Most events will have a single edit, a single event
|
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
|
will have multiple edits when a dissolve, wipe or key transition needs
|
||||||
to be performed.
|
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())
|
edits_audio = list(self._statements_with_audio_ext())
|
||||||
clip_names = self._clip_name_statements()
|
clip_names = self._clip_name_statements()
|
||||||
source_files = self._source_file_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:
|
if len(edits_audio) == 2:
|
||||||
start_name: Optional[StmtClipName] = None
|
start_name: StmtClipName | None = None
|
||||||
end_name: Optional[StmtClipName] = None
|
end_name: StmtClipName | None = None
|
||||||
|
|
||||||
for clip_name in clip_names:
|
for clip_name in clip_names:
|
||||||
if clip_name.affect == 'from':
|
if clip_name.affect == "from":
|
||||||
start_name = clip_name
|
start_name = clip_name
|
||||||
elif clip_name.affect == 'to':
|
elif clip_name.affect == "to":
|
||||||
end_name = clip_name
|
end_name = clip_name
|
||||||
|
|
||||||
the_zip.append([start_name, end_name])
|
the_zip.append([start_name, end_name])
|
||||||
@@ -54,6 +78,10 @@ class Event:
|
|||||||
else:
|
else:
|
||||||
the_zip.append([None] * len(edits_audio))
|
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):
|
if len(edits_audio) == len(source_files):
|
||||||
the_zip.append(source_files)
|
the_zip.append(source_files)
|
||||||
elif len(source_files) == 1:
|
elif len(source_files) == 1:
|
||||||
@@ -61,27 +89,31 @@ class Event:
|
|||||||
else:
|
else:
|
||||||
the_zip.append([None] * len(edits_audio))
|
the_zip.append([None] * len(edits_audio))
|
||||||
|
|
||||||
# attach trans name to last event
|
# attach effects name to last event
|
||||||
try:
|
try:
|
||||||
trans_statement = self._trans_name_statements()[0]
|
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)
|
trans_names.append(trans_statement)
|
||||||
the_zip.append(trans_names)
|
the_zip.append(trans_names)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
the_zip.append([None] * len(edits_audio))
|
the_zip.append([None] * len(edits_audio))
|
||||||
return [Edit(edit_statement=e1[0],
|
|
||||||
audio_ext_statement=e1[1],
|
return [
|
||||||
clip_name_statement=n1,
|
Edit(
|
||||||
source_file_statement=s1,
|
edit_statement=e1[0],
|
||||||
trans_name_statement=u1,
|
audio_ext_statement=e1[1],
|
||||||
asc_sop_statement=self._asc_sop_statement(),
|
clip_name_statement=n1,
|
||||||
asc_sat_statement=self._asc_sat_statement(),
|
source_file_statement=s1,
|
||||||
frmc_statement=self._frmc_statement())
|
trans_name_statement=u1,
|
||||||
for (e1, n1, s1, u1) in zip(*the_zip)]
|
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
|
@property
|
||||||
def unrecognized_statements(self) -> Generator[StmtUnrecognized, None,
|
def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]:
|
||||||
None]:
|
|
||||||
"""
|
"""
|
||||||
A generator for all the unrecognized statements in the event.
|
A generator for all the unrecognized statements in the event.
|
||||||
"""
|
"""
|
||||||
@@ -89,38 +121,37 @@ class Event:
|
|||||||
if type(s) is StmtUnrecognized:
|
if type(s) is StmtUnrecognized:
|
||||||
yield s
|
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]
|
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]
|
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]
|
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]
|
return [s for s in self.statements if type(s) is StmtSourceFile]
|
||||||
|
|
||||||
def _statements_with_audio_ext(self) -> Generator[
|
def _statements_with_audio_ext(
|
||||||
Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
|
self,
|
||||||
|
) -> Generator[tuple[StmtEvent, StmtAudioExt | None], None, None]:
|
||||||
|
|
||||||
if len(self.statements) == 1 and type(self.statements[0]) is StmtEvent:
|
if len(self.statements) == 1 and type(self.statements[0]) is StmtEvent:
|
||||||
yield (self.statements[0], None)
|
yield (self.statements[0], None)
|
||||||
|
|
||||||
else:
|
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:
|
if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
|
||||||
yield (s1, s2)
|
yield (s1, s2)
|
||||||
elif type(s1) is StmtEvent:
|
elif type(s1) is StmtEvent:
|
||||||
yield (s1, None)
|
yield (s1, None)
|
||||||
|
|
||||||
def _asc_sop_statement(self) -> Optional[StmtCdlSop]:
|
def _asc_sop_statement(self) -> StmtCdlSop | None:
|
||||||
return next((s for s in self.statements if type(s) is StmtCdlSop),
|
return next((s for s in self.statements if type(s) is StmtCdlSop), None)
|
||||||
None)
|
|
||||||
|
|
||||||
def _asc_sat_statement(self) -> Optional[StmtCdlSat]:
|
def _asc_sat_statement(self) -> StmtCdlSat | None:
|
||||||
return next((s for s in self.statements if type(s) is StmtCdlSat),
|
return next((s for s in self.statements if type(s) is StmtCdlSat), None)
|
||||||
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)
|
return next((s for s in self.statements if type(s) is StmtFrmc), None)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
|
|
||||||
from .parse_cmx_statements import (parse_cmx3600_statements)
|
|
||||||
from .edit_list import EditList
|
from .edit_list import EditList
|
||||||
|
from .parse_cmx_statements import parse_cmx3600_statements
|
||||||
|
|
||||||
|
|
||||||
def parse_cmx3600(f: TextIO, tolerant: bool = False) -> EditList:
|
def parse_cmx3600(f: TextIO, tolerant: bool = False) -> EditList:
|
||||||
|
|||||||
@@ -1,37 +1,63 @@
|
|||||||
# pycmx
|
# pycmx
|
||||||
# (c) 2018-2025 Jamie Hardt
|
# (c) 2018-2025 Jamie Hardt
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import TextIO, List
|
from typing import TextIO
|
||||||
|
|
||||||
from .cdl import AscSopComponents, Rgb
|
from .cdl import AscSopComponents, Rgb
|
||||||
|
from .statements import (
|
||||||
from .statements import (StmtCdlSat, StmtCdlSop, StmtCorruptRemark, StmtFrmc,
|
StmtAudioExt,
|
||||||
StmtRemark, StmtTitle, StmtUnrecognized, StmtFCM,
|
StmtCdlSat,
|
||||||
StmtAudioExt, StmtClipName, StmtEffectsName,
|
StmtCdlSop,
|
||||||
StmtEvent, StmtSourceFile, StmtSplitEdit)
|
StmtClipName,
|
||||||
|
StmtCorruptRemark,
|
||||||
|
StmtEffectsName,
|
||||||
|
StmtEvent,
|
||||||
|
StmtFCM,
|
||||||
|
StmtFrmc,
|
||||||
|
StmtRemark,
|
||||||
|
StmtSourceFile,
|
||||||
|
StmtSplitEdit,
|
||||||
|
StmtTitle,
|
||||||
|
StmtUnrecognized,
|
||||||
|
)
|
||||||
from .util import collimate
|
from .util import collimate
|
||||||
|
|
||||||
|
|
||||||
def parse_cmx3600_statements(file: TextIO,
|
def parse_cmx3600_statements(file: TextIO, tolerant: bool = False) -> list[object]:
|
||||||
tolerant: bool = False) -> List[object]:
|
|
||||||
"""
|
"""
|
||||||
Return a list of every statement in the file argument.
|
Return a list of every statement in the file argument.
|
||||||
"""
|
"""
|
||||||
lines = file.readlines()
|
lines = file.readlines()
|
||||||
return [_parse_cmx3600_line(line.strip(), line_number, tolerant)
|
return [
|
||||||
for (line_number, line) in enumerate(lines)]
|
_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]:
|
def _edl_column_widths(event_field_length, source_field_length) -> list[int]:
|
||||||
return [event_field_length, 2, source_field_length, 1,
|
return [
|
||||||
4, 2, # chans
|
event_field_length,
|
||||||
4, 1, # trans
|
2,
|
||||||
3, 1, # trans op
|
source_field_length,
|
||||||
11, 1,
|
1,
|
||||||
11, 1,
|
4,
|
||||||
11, 1,
|
2, # chans
|
||||||
11]
|
4,
|
||||||
|
1, # trans
|
||||||
|
3,
|
||||||
|
1, # trans op
|
||||||
|
11,
|
||||||
|
1,
|
||||||
|
11,
|
||||||
|
1,
|
||||||
|
11,
|
||||||
|
1,
|
||||||
|
11,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# def _edl_m2_column_widths():
|
# def _edl_m2_column_widths():
|
||||||
# return [2, # "M2"
|
# 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]
|
# 8,8,1,4,2,1,4,13,3,1,1]
|
||||||
|
|
||||||
|
|
||||||
def _parse_cmx3600_line(line: str, line_number: int,
|
def _parse_cmx3600_line(line: str, line_number: int, tolerant: bool = False) -> object:
|
||||||
tolerant: bool = False) -> object:
|
|
||||||
"""
|
"""
|
||||||
Parses a single CMX EDL line.
|
Parses a single CMX EDL line.
|
||||||
|
|
||||||
@@ -61,7 +86,8 @@ def _parse_cmx3600_line(line: str, line_number: int,
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return _parse_columns_for_standard_form(
|
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:
|
except EventFormError:
|
||||||
if tolerant:
|
if tolerant:
|
||||||
@@ -112,73 +138,88 @@ def _parse_extended_audio_channels(line, line_number):
|
|||||||
|
|
||||||
def _parse_remark(line, line_number) -> object:
|
def _parse_remark(line, line_number) -> object:
|
||||||
if line.startswith("FROM CLIP NAME:"):
|
if line.startswith("FROM CLIP NAME:"):
|
||||||
return StmtClipName(name=line[15:].strip(), affect="from",
|
return StmtClipName(
|
||||||
line_number=line_number)
|
name=line[15:].strip(), affect="from", line_number=line_number
|
||||||
|
)
|
||||||
elif line.startswith("TO CLIP NAME:"):
|
elif line.startswith("TO CLIP NAME:"):
|
||||||
return StmtClipName(name=line[13:].strip(), affect="to",
|
return StmtClipName(
|
||||||
line_number=line_number)
|
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(),
|
return StmtSourceFile(filename=line[12:].strip(), line_number=line_number)
|
||||||
line_number=line_number)
|
|
||||||
elif line.startswith("ASC_SOP"):
|
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]]] = \
|
v1: list[list[tuple[str, str]]] = [
|
||||||
[re.findall(r'(-?\d+(\.\d+)?)', a) for a in group_patterns]
|
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]
|
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)
|
return StmtRemark(line, line_number)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return StmtCdlSop(line=line,
|
return StmtCdlSop(
|
||||||
cdl_sop=AscSopComponents(
|
line=line,
|
||||||
slope=Rgb(red=float(v[0][0]),
|
cdl_sop=AscSopComponents(
|
||||||
green=float(v[0][1]),
|
slope=Rgb(
|
||||||
blue=float(v[0][2])),
|
red=float(v[0][0]),
|
||||||
offset=Rgb(red=float(v[1][0]),
|
green=float(v[0][1]),
|
||||||
green=float(v[1][1]),
|
blue=float(v[0][2]),
|
||||||
blue=float(v[1][2])),
|
),
|
||||||
power=Rgb(red=float(v[2][0]),
|
offset=Rgb(
|
||||||
green=float(v[2][1]),
|
red=float(v[1][0]),
|
||||||
blue=float(v[2][2]))
|
green=float(v[1][1]),
|
||||||
),
|
blue=float(v[1][2]),
|
||||||
line_number=line_number)
|
),
|
||||||
|
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:
|
except ValueError as e:
|
||||||
return StmtCorruptRemark('ASC_SOP', e, line_number)
|
return StmtCorruptRemark("ASC_SOP", e, line_number)
|
||||||
|
|
||||||
elif line.startswith("ASC_SAT"):
|
elif line.startswith("ASC_SAT"):
|
||||||
value = re.findall(r'(-?\d+(\.\d+)?)', line)
|
value = re.findall(r"(-?\d+(\.\d+)?)", line)
|
||||||
|
|
||||||
if len(value) != 1:
|
if len(value) != 1:
|
||||||
return StmtRemark(line, line_number)
|
return StmtRemark(line, line_number)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return StmtCdlSat(value=float(value[0][0]),
|
return StmtCdlSat(value=float(value[0][0]), line_number=line_number)
|
||||||
line_number=line_number)
|
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return StmtCorruptRemark('ASC_SAT', e, line_number)
|
return StmtCorruptRemark("ASC_SAT", e, line_number)
|
||||||
|
|
||||||
elif line.startswith("FRMC"):
|
elif line.startswith("FRMC"):
|
||||||
match = re.match(r'^FRMC START:\s*(\d+)\s+FRMC END:\s*(\d+)'
|
match = re.match(
|
||||||
r'\s+FRMC DURATION:\s*(\d+)', line, re.IGNORECASE)
|
r"^FRMC START:\s*(\d+)\s+FRMC END:\s*(\d+)"
|
||||||
|
r"\s+FRMC DURATION:\s*(\d+)",
|
||||||
|
line,
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
if match is None:
|
if match is None:
|
||||||
return StmtCorruptRemark('FRMC', None, line_number)
|
return StmtCorruptRemark("FRMC", None, line_number)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return StmtFrmc(start=int(match.group(1)),
|
return StmtFrmc(
|
||||||
end=int(match.group(2)),
|
start=int(match.group(1)),
|
||||||
duration=int(match.group(3)),
|
end=int(match.group(2)),
|
||||||
line_number=line_number)
|
duration=int(match.group(3)),
|
||||||
|
line_number=line_number,
|
||||||
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return StmtCorruptRemark('FRMC', e, line_number)
|
return StmtCorruptRemark("FRMC", e, line_number)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return StmtRemark(text=line, line_number=line_number)
|
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")
|
is_video = split_type.startswith("VIDEO")
|
||||||
|
|
||||||
split_delay = line[24:35]
|
split_delay = line[24:35]
|
||||||
return StmtSplitEdit(video=is_video, delay=split_delay,
|
return StmtSplitEdit(video=is_video, delay=split_delay, line_number=line_number)
|
||||||
line_number=line_number)
|
|
||||||
|
|
||||||
|
|
||||||
# def _parse_motion_memory(line, line_number):
|
# def _parse_motion_memory(line, line_number):
|
||||||
# return StmtMotionMemory(source="", fps="")
|
# return StmtMotionMemory(source="", fps="")
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class EventFormError(RuntimeError):
|
class EventFormError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -210,9 +251,9 @@ def _parse_unrecognized(line, line_number):
|
|||||||
return StmtUnrecognized(content=line, line_number=line_number)
|
return StmtUnrecognized(content=line, line_number=line_number)
|
||||||
|
|
||||||
|
|
||||||
def _parse_columns_for_standard_form(line: str, event_field_length: int,
|
def _parse_columns_for_standard_form(
|
||||||
source_field_length: int,
|
line: str, event_field_length: int, source_field_length: int, line_number: int
|
||||||
line_number: int):
|
):
|
||||||
# breakpoint()
|
# breakpoint()
|
||||||
col_widths = _edl_column_widths(event_field_length, source_field_length)
|
col_widths = _edl_column_widths(event_field_length, source_field_length)
|
||||||
|
|
||||||
@@ -227,35 +268,45 @@ def _parse_columns_for_standard_form(line: str, event_field_length: int,
|
|||||||
if len(channels) == 0 or len(trans) == 0:
|
if len(channels) == 0 or len(trans) == 0:
|
||||||
raise EventFormError()
|
raise EventFormError()
|
||||||
|
|
||||||
return StmtEvent(event=column_strings[0],
|
return StmtEvent(
|
||||||
source=column_strings[2].strip(),
|
event=int(column_strings[0]),
|
||||||
channels=channels,
|
source=column_strings[2].strip(),
|
||||||
trans=trans,
|
channels=channels,
|
||||||
trans_op=column_strings[8].strip(),
|
trans=trans,
|
||||||
source_in=column_strings[10].strip(),
|
trans_op=column_strings[8].strip(),
|
||||||
source_out=column_strings[12].strip(),
|
source_in=column_strings[10].strip(),
|
||||||
record_in=column_strings[14].strip(),
|
source_out=column_strings[12].strip(),
|
||||||
record_out=column_strings[16].strip(),
|
record_in=column_strings[14].strip(),
|
||||||
line_number=line_number,
|
record_out=column_strings[16].strip(),
|
||||||
source_field_size=source_field_length)
|
line_number=line_number,
|
||||||
|
source_field_size=source_field_length,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_columns_tolerant(line: str, line_number: int):
|
def _parse_columns_tolerant(line: str, line_number: int):
|
||||||
pattern = re.compile(r'^\s*(\d+)\s+(.{8,128}?)\s+'
|
pattern = re.compile(
|
||||||
r'(V|A|A2|AA|NONE|AA/V|A2/V|B)\s+'
|
r"^\s*(\d+)\s+(.{8,128}?)\s+"
|
||||||
r'(C|D|W|KB|K|KO)\s+(\d*)\s+(\d\d.\d\d.\d\d.\d\d)\s'
|
r"(V|A|A2|AA|NONE|AA/V|A2/V|B)\s+"
|
||||||
r'(\d\d.\d\d.\d\d.\d\d)\s(\d\d.\d\d.\d\d.\d\d)\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)'
|
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)
|
match = pattern.match(line)
|
||||||
if match:
|
if match:
|
||||||
return StmtEvent(event=int(match.group(1)), source=match.group(2),
|
return StmtEvent(
|
||||||
channels=match.group(3), trans=match.group(4),
|
event=int(match.group(1)),
|
||||||
trans_op=match.group(5), source_in=match.group(6),
|
source=match.group(2),
|
||||||
source_out=match.group(7), record_in=match.group(8),
|
channels=match.group(3),
|
||||||
record_out=match.group(9), line_number=line_number,
|
trans=match.group(4),
|
||||||
source_field_size=len(match.group(2)))
|
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:
|
else:
|
||||||
return StmtUnrecognized(line, line_number)
|
return StmtUnrecognized(line, line_number)
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ from typing import Any, NamedTuple
|
|||||||
|
|
||||||
from .cdl import AscSopComponents
|
from .cdl import AscSopComponents
|
||||||
|
|
||||||
# type str = str
|
|
||||||
|
|
||||||
|
|
||||||
class StmtTitle(NamedTuple):
|
class StmtTitle(NamedTuple):
|
||||||
title: str
|
title: str
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# pycmx
|
# pycmx
|
||||||
# (c) 2023 Jamie Hardt
|
# (c) 2023 Jamie Hardt
|
||||||
|
|
||||||
from typing import Optional
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
class Transition:
|
class Transition:
|
||||||
@@ -22,9 +22,9 @@ class Transition:
|
|||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def kind(self) -> Optional[str]:
|
def kind(self) -> str | None:
|
||||||
"""
|
"""
|
||||||
Return the kind of transition: Cut, Wipe, etc
|
Return the kind of transition: Cut, Wipe, etc.
|
||||||
"""
|
"""
|
||||||
if self.cut:
|
if self.cut:
|
||||||
return Transition.Cut
|
return Transition.Cut
|
||||||
@@ -42,28 +42,29 @@ class Transition:
|
|||||||
@property
|
@property
|
||||||
def cut(self) -> bool:
|
def cut(self) -> bool:
|
||||||
"`True` if this transition is a cut."
|
"`True` if this transition is a cut."
|
||||||
return self.transition == 'C'
|
return self.transition == "C"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dissolve(self) -> bool:
|
def dissolve(self) -> bool:
|
||||||
"`True` if this traansition is a dissolve."
|
"`True` if this traansition is a dissolve."
|
||||||
return self.transition == 'D'
|
return self.transition == "D"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wipe(self) -> bool:
|
def wipe(self) -> bool:
|
||||||
"`True` if this transition is a wipe."
|
"`True` if this transition is a wipe."
|
||||||
return self.transition.startswith('W')
|
return self.transition.startswith("W")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def effect_duration(self) -> int:
|
def effect_duration(self) -> int:
|
||||||
"""The duration of this transition, in frames of the record target.
|
"""
|
||||||
|
The duration of this transition, in frames of the record target.
|
||||||
|
|
||||||
In the event of a key event, this is the duration of the fade in.
|
In the event of a key event, this is the duration of the fade in.
|
||||||
"""
|
"""
|
||||||
return int(self.operand)
|
return int(self.operand)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wipe_number(self) -> Optional[int]:
|
def wipe_number(self) -> int | None:
|
||||||
"Wipes are identified by a particular number."
|
"Wipes are identified by a particular number."
|
||||||
if self.wipe:
|
if self.wipe:
|
||||||
return int(self.transition[1:])
|
return int(self.transition[1:])
|
||||||
|
|||||||
+4
-1
@@ -3,7 +3,10 @@
|
|||||||
|
|
||||||
# Utility functions
|
# 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
|
Split a list-type thing, like a string, into slices that are column_widths
|
||||||
length.
|
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 unittest import TestCase
|
||||||
|
|
||||||
from pycmx import parse_cmx3600
|
from pycmx import parse_cmx3600
|
||||||
|
|||||||
+44
-47
@@ -5,64 +5,65 @@ import pycmx
|
|||||||
|
|
||||||
class TestParse(TestCase):
|
class TestParse(TestCase):
|
||||||
|
|
||||||
files = ["INS4_R1_010417.edl",
|
def setUp(self) -> None:
|
||||||
"INS4_R1_DX_092117.edl",
|
self.files = [
|
||||||
"STP R1 v082517.edl",
|
"INS4_R1_010417.edl",
|
||||||
"ToD_R4_LOCK3.1_030618_Video.edl",
|
"INS4_R1_DX_092117.edl",
|
||||||
"TEST.edl",
|
"STP R1 v082517.edl",
|
||||||
"test_edl_cdl.edl",
|
"ToD_R4_LOCK3.1_030618_Video.edl",
|
||||||
"INS4_R1_DX_092117.edl"
|
"TEST.edl",
|
||||||
]
|
"test_edl_cdl.edl",
|
||||||
|
"INS4_R1_DX_092117.edl",
|
||||||
|
]
|
||||||
|
|
||||||
|
return super().setUp()
|
||||||
|
|
||||||
def test_event_counts(self):
|
def test_event_counts(self):
|
||||||
|
|
||||||
counts = [287, 466, 250, 376, 120, 3, 466]
|
counts = [287, 466, 250, 376, 120, 3, 466]
|
||||||
|
|
||||||
for fn, count in zip(type(self).files, counts):
|
for fn, count in zip(self.files, counts):
|
||||||
with open("tests/edls/" + fn, 'r') as f:
|
with open("tests/edls/" + fn, "r") as f:
|
||||||
edl = pycmx.parse_cmx3600(f)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
actual = len(list(edl.events))
|
actual = len(list(edl.events))
|
||||||
self.assertTrue(actual == count,
|
self.assertTrue(
|
||||||
"expected %i in file %s but found %i"
|
actual == count,
|
||||||
% (count, fn, actual))
|
f"expected {count} in file {fn} but found {actual}"
|
||||||
|
)
|
||||||
|
|
||||||
def test_list_sanity(self):
|
def test_list_sanity(self):
|
||||||
for fn in type(self).files:
|
for fn in self.files:
|
||||||
with open("tests/edls/" + fn, 'r') as f:
|
with open("tests/edls/" + fn, "r") as f:
|
||||||
edl = pycmx.parse_cmx3600(f)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
self.assertTrue(type(edl.title) is str)
|
self.assertTrue(type(edl.title) is str)
|
||||||
self.assertTrue(len(edl.title) > 0)
|
self.assertTrue(len(edl.title) > 0)
|
||||||
|
|
||||||
def test_event_sanity(self):
|
def test_event_sanity(self):
|
||||||
for fn in type(self).files:
|
for fn in self.files:
|
||||||
path = "tests/edls/" + fn
|
path = "tests/edls/" + fn
|
||||||
with open(path, 'r') as f:
|
with open(path, "r") as f:
|
||||||
edl = pycmx.parse_cmx3600(f)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
for index, event in enumerate(edl.events):
|
for index, event in enumerate(edl.events):
|
||||||
self.assertTrue(len(event.edits) > 0,
|
self.assertTrue(len(event.edits) > 0, f"Failed for {path}")
|
||||||
f"Failed for {path}")
|
self.assertEqual(event.number, index + 1, f"Failed for {path}")
|
||||||
self.assertEqual(event.number, index + 1,
|
|
||||||
f"Failed for {path}")
|
|
||||||
|
|
||||||
def test_events(self):
|
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)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
events = list(edl.events)
|
events = list(edl.events)
|
||||||
|
|
||||||
self.assertEqual(events[0].number, 1)
|
self.assertEqual(events[0].number, 1)
|
||||||
self.assertEqual(events[0].edits[0].source, "OY_HEAD_")
|
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].clip_name, "HEAD LEADER MONO")
|
||||||
self.assertEqual(
|
self.assertEqual(events[0].edits[0].source_file, "OY_HEAD_LEADER.MOV")
|
||||||
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_in, "00:00:00:00")
|
||||||
self.assertEqual(events[0].edits[0].source_out, "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_in, "01:00:00:00")
|
||||||
self.assertEqual(events[0].edits[0].record_out, "01:00:08:00")
|
self.assertEqual(events[0].edits[0].record_out, "01:00:08:00")
|
||||||
self.assertTrue(
|
self.assertTrue(events[0].edits[0].transition.kind == pycmx.Transition.Cut)
|
||||||
events[0].edits[0].transition.kind == pycmx.Transition.Cut)
|
|
||||||
|
|
||||||
def test_channel_map(self):
|
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)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
events = list(edl.events)
|
events = list(edl.events)
|
||||||
self.assertFalse(events[0].edits[0].channels.video)
|
self.assertFalse(events[0].edits[0].channels.video)
|
||||||
@@ -72,7 +73,7 @@ class TestParse(TestCase):
|
|||||||
self.assertTrue(events[2].edits[0].channels.audio)
|
self.assertTrue(events[2].edits[0].channels.audio)
|
||||||
|
|
||||||
def test_multi_edit_events(self):
|
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)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
events = list(edl.events)
|
events = list(edl.events)
|
||||||
|
|
||||||
@@ -80,25 +81,24 @@ class TestParse(TestCase):
|
|||||||
self.assertEqual(len(events[42].edits), 2)
|
self.assertEqual(len(events[42].edits), 2)
|
||||||
|
|
||||||
self.assertEqual(events[42].edits[0].source, "TC_R1_V1")
|
self.assertEqual(events[42].edits[0].source, "TC_R1_V1")
|
||||||
self.assertEqual(events[42].edits[0].clip_name,
|
self.assertEqual(
|
||||||
"TC R1 V1.2 TEMP1 FX ST.WAV")
|
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_in, "00:00:00:00")
|
||||||
self.assertEqual(events[42].edits[0].source_out, "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_in, "01:08:56:09")
|
||||||
self.assertEqual(events[42].edits[0].record_out, "01:08:56:09")
|
self.assertEqual(events[42].edits[0].record_out, "01:08:56:09")
|
||||||
self.assertTrue(
|
self.assertTrue(events[42].edits[0].transition.kind == pycmx.Transition.Cut)
|
||||||
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].source, "TC_R1_V6")
|
||||||
self.assertEqual(events[42].edits[1].clip_name,
|
self.assertEqual(events[42].edits[1].clip_name, "TC R1 V6 TEMP2 ST FX.WAV")
|
||||||
"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_in, "00:00:00:00")
|
||||||
self.assertEqual(events[42].edits[1].source_out, "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_in, "01:08:56:09")
|
||||||
self.assertEqual(events[42].edits[1].record_out, "01:08:56:11")
|
self.assertEqual(events[42].edits[1].record_out, "01:08:56:11")
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
events[42].edits[1].transition.kind ==
|
events[42].edits[1].transition.kind == pycmx.Transition.Dissolve
|
||||||
pycmx.Transition.Dissolve)
|
)
|
||||||
|
|
||||||
def test_line_numbers(self):
|
def test_line_numbers(self):
|
||||||
with open("tests/edls/ToD_R4_LOCK3.1_030618_Video.edl") as f:
|
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:
|
with open("tests/edls/test_25.edl", "r") as f:
|
||||||
edl = pycmx.parse_cmx3600(f)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
events = list(edl.events)
|
events = list(edl.events)
|
||||||
self.assertEqual(
|
self.assertEqual(events[4].edits[1].transition.name, "CROSS DISSOLVE")
|
||||||
events[4].edits[1].transition.name, "CROSS DISSOLVE")
|
|
||||||
|
|
||||||
def test_adobe_wide(self):
|
def test_adobe_wide(self):
|
||||||
with open("tests/edls/adobe_dai109_test.txt", 'r',
|
with open("tests/edls/adobe_dai109_test.txt", "r", encoding="ISO-8859-1") as f:
|
||||||
encoding='ISO-8859-1') as f:
|
|
||||||
edl = pycmx.parse_cmx3600(f)
|
edl = pycmx.parse_cmx3600(f)
|
||||||
events = list(edl.events)
|
events = list(edl.events)
|
||||||
|
|
||||||
@@ -131,12 +129,11 @@ class TestParse(TestCase):
|
|||||||
for event in edl.events:
|
for event in edl.events:
|
||||||
if event.number == 42:
|
if event.number == 42:
|
||||||
self.assertEqual(len(event.edits), 1)
|
self.assertEqual(len(event.edits), 1)
|
||||||
self.assertEqual(event.edits[0].source,
|
self.assertEqual(event.edits[0].source, "M018C0005_240925_1F4L13")
|
||||||
"M018C0005_240925_1F4L13")
|
self.assertEqual(
|
||||||
self.assertEqual(event.edits[0].transition.kind,
|
event.edits[0].transition.kind, pycmx.Transition.Cut
|
||||||
pycmx.Transition.Cut)
|
)
|
||||||
self.assertEqual(event.edits[0].source_in,
|
self.assertEqual(event.edits[0].source_in, "18:44:20:12")
|
||||||
"18:44:20:12")
|
|
||||||
|
|
||||||
def test_cdl(self):
|
def test_cdl(self):
|
||||||
with open("tests/edls/cdl_example01.edl", "r") as f:
|
with open("tests/edls/cdl_example01.edl", "r") as f:
|
||||||
@@ -152,7 +149,7 @@ class TestParse(TestCase):
|
|||||||
sat = event.edits[0].asc_sat
|
sat = event.edits[0].asc_sat
|
||||||
self.assertIsNotNone(sat)
|
self.assertIsNotNone(sat)
|
||||||
assert sat
|
assert sat
|
||||||
self.assertEqual(sat, float('0.9640'))
|
self.assertEqual(sat, float("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:
|
||||||
|
|||||||
Reference in New Issue
Block a user