Compare commits

..

40 Commits

Author SHA1 Message Date
jamie 9c4f6faf2a Ruff 2026-09-19 15:30:05 -07:00
jamie 754e4d5251 ruffage 2026-09-19 15:20:18 -07:00
jamie 888d9aa5d9 ruff 2026-09-19 14:32:35 -07:00
jamie bd0b09d1db ruff for Reaper script 2026-09-19 14:31:46 -07:00
jamie 3c15237ed2 Renamed method call here for clarity 2026-09-17 21:19:20 -07:00
jamie ab19793bd1 Merge branch 'master' of https://github.com/iluvcapra/ptulsconv into bug-ruff-2026 2026-09-17 20:41:35 -07:00
jamie 7730f6be23 Ruff fixes in progress 2026-09-17 20:35:11 -07:00
jamie e286132101 Ruff fixes in progress 2026-09-17 20:26:12 -07:00
jamie 4afe4b7a3e Ruff fixes in progress 2026-09-17 20:25:28 -07:00
jamie cb8de2d785 Ruff fixes in progress 2026-09-17 20:20:54 -07:00
jamie 9e9ad75f77 Ruff fixes in progress 2026-09-17 20:06:46 -07:00
jamie f7808e2236 Ruff format 2026-09-17 15:23:42 -07:00
Jamie Hardt 90f78c8fa2 Update CONTRIBUTING.md
Updating Agents policy
2026-05-03 12:16:07 -07:00
Jamie Hardt ebd0b54b6a Update .readthedocs.yaml 2026-03-30 11:54:59 -07:00
Jamie Hardt 98958c42f4 Update .readthedocs.yaml 2026-03-30 11:50:00 -07:00
Jamie Hardt bf9ff7b00f Documentation updates (#18)
* Update .readthedocs.yaml

Updated RTD version to `ubuntu-lts-latest`

* Update .readthedocs.yaml

RTD config fix
2026-03-30 11:46:52 -07:00
Jamie Hardt c45ca3a8fd Decline contributions from Large Language Models
Added a note regarding contributions from Large Language Models.
2026-02-15 15:49:26 -08:00
Jamie Hardt 1fd86a6d7d Switching to ruff linter (#17)
* Migrating to uv build and manager

* Updated Flake8 settings

* Updating github workflow

* Update python-package.yml

Removed redundant flake8 run

* Twiddle

* Version number in banner

* Changing to ruff linter

* Tweaking workflow

* Update python-package.yml

* Update __init__.py

* Update __init__.py

* removing flake8 file

* Doc notes

* Twiddling with RtD

---------

Co-authored-by: Jamie Hardt <jamie@squad51.us>
2026-01-21 21:56:05 -08:00
jamie e593996b65 Revert "Twiddling with RtD"
This reverts commit 32191791c6.
2025-09-20 23:20:24 -07:00
jamie 32191791c6 Twiddling with RtD 2025-09-20 23:00:49 -07:00
jamie 1ff910e4d2 Doc notes 2025-09-20 22:34:58 -07:00
jamie 0e8a77f548 removing flake8 file 2025-09-20 22:26:21 -07:00
Jamie Hardt 7058397f6f Update __init__.py 2025-09-20 22:24:10 -07:00
Jamie Hardt 16d7befd9f Update __init__.py 2025-09-20 22:21:56 -07:00
Jamie Hardt 2a3f4a7c18 Update python-package.yml 2025-09-20 22:19:14 -07:00
jamie d7fff23e5e Tweaking workflow 2025-09-20 22:12:55 -07:00
Jamie Hardt 757c3171e7 Merge branch 'master' into feat-uv 2025-09-20 22:10:15 -07:00
jamie 48dd7de07e Changing to ruff linter 2025-09-20 22:07:33 -07:00
Jamie Hardt dc259d54aa Update README.md 2025-09-20 16:39:06 -07:00
Jamie Hardt 09ed12fc8f Migrating to uv build and manager (#16)
* Migrating to uv build and manager

* Updated Flake8 settings

* Updating github workflow

* Update python-package.yml

* Removed redundant flake8 run

* Version number in banner

---------

Co-authored-by: Jamie Hardt <jamie@squad51.us>
2025-09-20 10:04:15 -07:00
jamie 2a69bf47c8 Version number in banner 2025-09-20 10:02:44 -07:00
jamie 01d2374a9b Merge branch 'feat-uv' of https://github.com/iluvcapra/ptulsconv into feat-uv 2025-09-20 09:47:07 -07:00
jamie 68e7eb89a9 Twiddle 2025-09-20 09:46:46 -07:00
Jamie Hardt 517fe3526a Update python-package.yml
Removed redundant flake8 run
2025-09-20 08:56:34 -07:00
jamie 7789c0df4c Updating github workflow 2025-09-19 10:43:20 -07:00
jamie d97dcc3e08 Updated Flake8 settings 2025-09-19 10:41:41 -07:00
jamie 615b2b5a2a Migrating to uv build and manager 2025-09-19 10:36:28 -07:00
jamie 4cd6ba1772 Updated for latest ptsl-py
Major version change, dependency changes
2025-09-19 09:52:24 -07:00
jamie 1942b323b3 Merge branch 'master' of https://github.com/iluvcapra/ptulsconv 2025-09-08 16:27:19 -07:00
jamie 7d297a7564 Nudged version 2025-09-08 16:26:40 -07:00
59 changed files with 2980 additions and 2453 deletions
-4
View File
@@ -1,4 +0,0 @@
[flake8]
per-file-ignores =
ptulsconv/__init__.py: F401
ptulsconv/docparser/__init__.py: F401
+4 -8
View File
@@ -27,15 +27,11 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
pip install -e .
- name: Lint with flake8
pip install .
pip install --group dev .
- name: Lint with ruff
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 ptulsconv tests --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 ptulsconv tests --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
ruff check src
- name: Test with pytest
run: |
pytest
flake8 ptulsconv
+1
View File
@@ -109,3 +109,4 @@ lcov.info
.vim
.vscode
uv.lock
+18 -6
View File
@@ -7,13 +7,20 @@ 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.10"
# You can also specify other tool versions:
# nodejs: "16"
# rust: "1.55"
# golang: "1.17"
python: "3.13"
jobs:
pre_install:
- pip install --upgrade pip
- pip install --upgrade --upgrade-strategy only-if-needed --group doc .
# build:
# html:
# - python -m sphinx -T -b html -d _build/doctrees -D language=en . $READTHEDOCS_OUTPUT/html
post_build:
- echo "Command run at 'post_build' step"
- echo `date`
# Build documentation in the docs/ directory with Sphinx
sphinx:
@@ -23,10 +30,15 @@ sphinx:
formats:
- pdf
# FIXME: We should be installing the `doc` dependency group and not the `doc`
# extra.
#Optionally declare the Python requirements required to build your docs
python:
install:
- method: pip
path: .
extra_requirements:
- doc
+35 -1
View File
@@ -1,4 +1,6 @@
# Contributing to ptulsconv
# Contributing
Contributions to `ptulsconv` are welcome!
## Testing
@@ -7,3 +9,35 @@ Before submitting PRs or patches, please make sure your branch passes all of the
```sh
~/ptulsconv$ pytest
```
## Regarding use of Agents
`ptulsconv` 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.
+4
View File
@@ -23,6 +23,10 @@ The easiest way to install on your site is to use `pip`:
% pip3 install ptulsconv
If you are using `uv` you can also do a tool install...
% uv tool install ptulsconv
This will install the necessary libraries on your host and gives you
command-line access to the tool through an entry-point `ptulsconv`. In a
terminal window type `ptulsconv -h` for a list of available options.
+14 -17
View File
@@ -4,19 +4,18 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import importlib
import sys
import os
import sys
sys.path.insert(0, os.path.abspath("../.."))
print(sys.path)
import ptulsconv
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'ptulsconv'
copyright = '2019-2025 Jamie Hardt. All rights reserved'
project = "ptulsconv"
copyright = "2019-2025 Jamie Hardt. All rights reserved"
version = "Version 2"
release = importlib.metadata.version("ptulsconv")
@@ -24,31 +23,29 @@ release = importlib.metadata.version("ptulsconv")
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
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",
]
templates_path = ['_templates']
templates_path = ["_templates"]
exclude_patterns = []
master_doc = 'index'
master_doc = "index"
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"]
latex_documents = [
(master_doc, 'ptulsconv.tex', u'ptulsconv Documentation',
u'Jamie Hardt', 'manual'),
(master_doc, "ptulsconv.tex", "ptulsconv Documentation", "Jamie Hardt", "manual"),
]
@@ -67,7 +64,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 -------------------------------------------------
-128
View File
@@ -1,128 +0,0 @@
from optparse import OptionParser, OptionGroup
import datetime
import sys
from ptulsconv import __name__
import ptulsconv
from ptulsconv.commands import convert
from ptulsconv.reporting import print_status_style, \
print_banner_style, print_section_header_style, \
print_fatal_error
def dump_field_map(output=sys.stdout):
from ptulsconv.docparser.tag_mapping import TagMapping
from ptulsconv.docparser.adr_entity import ADRLine, GenericEvent
TagMapping.print_rules(GenericEvent, output=output)
TagMapping.print_rules(ADRLine, output=output)
def dump_formats():
print_section_header_style("`raw` format:")
sys.stderr.write("A JSON document of the parsed Pro Tools export.\n")
print_section_header_style("`tagged` Format:")
sys.stderr.write(
"A JSON document containing one record for each clip, with\n"
"all tags parsed and all tagging rules applied. \n")
print_section_header_style("`doc` format:")
sys.stderr.write("Creates a directory with folders for different types\n"
"of ADR reports.\n\n")
def main():
"""Entry point for the command-line invocation"""
parser = OptionParser()
parser.usage = "ptulsconv [options] [TEXT_EXPORT.txt]"
parser.add_option('-f', '--format',
dest='output_format',
metavar='FMT',
choices=['raw', 'tagged', 'doc'],
default='doc',
help='Set output format, `raw`, `tagged`, `doc`.')
parser.add_option('-m', '--movie-opts',
dest='movie_opts',
metavar="MOVIE_OPTS",
help="Set movie options")
warn_options = OptionGroup(title="Warning and Validation Options",
parser=parser)
warn_options.add_option('-W', action='store_false',
dest='warnings',
default=True,
help='Suppress warnings for common '
'errors (missing code numbers etc.)')
parser.add_option_group(warn_options)
informational_options = OptionGroup(title="Informational Options",
parser=parser,
description='Print useful '
'information '
'and exit without processing '
'input files.')
informational_options.add_option(
'--show-formats',
dest='show_formats',
action='store_true',
default=False,
help='Display helpful information about the available '
'output formats.')
informational_options.add_option(
'--show-available-tags',
dest='show_tags',
action='store_true',
default=False,
help='Display tag mappings for the FMP XML output style '
'and exit.')
parser.add_option_group(informational_options)
print_banner_style(ptulsconv.__name__)
print_banner_style(ptulsconv.__copyright__)
(options, args) = parser.parse_args(sys.argv)
print_section_header_style("Startup")
print_status_style("This run started %s" %
(datetime.datetime.now().isoformat()))
if options.show_tags:
dump_field_map()
sys.exit(0)
elif options.show_formats:
dump_formats()
sys.exit(0)
try:
major_mode = options.output_format
if len(args) < 2:
print_status_style(
"No input file provided, will connect to Pro Tools "
"with PTSL...")
convert(major_mode=major_mode,
warnings=options.warnings)
else:
convert(input_file=args[1],
major_mode=major_mode,
warnings=options.warnings)
except FileNotFoundError as e:
print_fatal_error("Error trying to read input file")
raise e
except Exception as e:
import traceback
print_fatal_error("Error trying to convert file")
print("\033[31m" + e.__repr__() + "\033[0m", file=sys.stderr)
print(traceback.format_exc())
if __name__ == "__main__":
main()
-137
View File
@@ -1,137 +0,0 @@
"""
This module defines classes and methods for converting :class:`Event` objects
into :class:`ADRLine` objects.
"""
from ptulsconv.docparser.tag_compiler import Event
from typing import Optional, List, Tuple
from dataclasses import dataclass
from fractions import Fraction
from ptulsconv.docparser.tag_mapping import TagMapping
def make_entities(from_events: List[Event]) -> Tuple[List['GenericEvent'],
List['ADRLine']]:
"""
Accepts a list of Events and converts them into either ADRLine events or
GenricEvents by calling :func:`make_entity` on each member.
:param from_events: A list of `Event` objects.
:returns: A tuple of two lists, the first containing :class:`GenericEvent`
and the second containing :class:`ADRLine`.
"""
generic_events = list()
adr_lines = list()
for event in from_events:
result = make_entity(event)
if type(result) is ADRLine:
adr_lines.append(result)
elif type(result) is GenericEvent:
generic_events.append(result)
return generic_events, adr_lines
def make_entity(from_event: Event) -> Optional[object]:
"""
Accepts an event and creates either an :class:`ADRLine` or a
:class:`GenericEvent`. An event is an "ADRLine" if it has a cue number/"QN"
tag field.
:param from_event: An :class:`Event`.
"""
instance = GenericEvent
tag_map = GenericEvent.tag_mapping
if 'QN' in from_event.tags.keys():
instance = ADRLine
tag_map += ADRLine.tag_mapping
new = instance()
TagMapping.apply_rules(tag_map, from_event.tags,
from_event.clip_name, from_event.track_name,
from_event.session_name, new)
new.start = from_event.start
new.finish = from_event.finish
return new
@dataclass
class GenericEvent:
title: str = ""
supervisor: Optional[str] = None
client: Optional[str] = None
scene: Optional[str] = None
version: Optional[str] = None
reel: Optional[str] = None
start: Fraction = Fraction(0, 1)
finish: Fraction = Fraction(0, 1)
omitted: bool = False
note: Optional[str] = None
requested_by: Optional[str] = None
tag_mapping = [
TagMapping(source='Title', target="title",
alt=TagMapping.ContentSource.Session),
TagMapping(source="Supv", target="supervisor"),
TagMapping(source="Client", target="client"),
TagMapping(source="Sc", target="scene"),
TagMapping(source="Ver", target="version"),
TagMapping(source="Reel", target="reel"),
TagMapping(source="Note", target="note"),
TagMapping(source="Rq", target="requested_by"),
TagMapping(source="OMIT", target="omitted",
formatter=(lambda x: len(x) > 0)),
]
@dataclass
class ADRLine(GenericEvent):
priority: Optional[int] = None
cue_number: Optional[str] = None
character_id: Optional[str] = None
character_name: Optional[str] = None
actor_name: Optional[str] = None
prompt: Optional[str] = None
reason: Optional[str] = None
time_budget_mins: Optional[float] = None
spot: Optional[str] = None
shot: Optional[str] = None
effort: bool = False
tv: bool = False
tbw: bool = False
adlib: bool = False
optional: bool = False
tag_mapping = [
TagMapping(source="P", target="priority"),
TagMapping(source="QN", target="cue_number"),
TagMapping(source="CN", target="character_id"),
TagMapping(source="Char", target="character_name",
alt=TagMapping.ContentSource.Track),
TagMapping(source="Actor", target="actor_name"),
TagMapping(source="Line", target="prompt",
alt=TagMapping.ContentSource.Clip),
TagMapping(source="R", target="reason"),
TagMapping(source="Mins", target="time_budget_mins",
formatter=(lambda n: float(n))),
TagMapping(source="Spot", target="spot"),
TagMapping(source="Shot", target="shot"),
TagMapping(source="EFF", target="effort",
formatter=(lambda x: len(x) > 0)),
TagMapping(source="TV", target="tv",
formatter=(lambda x: len(x) > 0)),
TagMapping(source="TBW", target="tbw",
formatter=(lambda x: len(x) > 0)),
TagMapping(source="ADLIB", target="adlib",
formatter=(lambda x: len(x) > 0)),
TagMapping(source="OPT", target="optional",
formatter=(lambda x: len(x) > 0))
]
-200
View File
@@ -1,200 +0,0 @@
from fractions import Fraction
from ptulsconv.broadcast_timecode import TimecodeFormat
from typing import Tuple, List, Iterator
class SessionDescriptor:
header: "HeaderDescriptor"
files: List["FileDescriptor"]
clips: List["ClipDescriptor"]
plugins: List["PluginDescriptor"]
tracks: List["TrackDescriptor"]
markers: List["MarkerDescriptor"]
def __init__(self, **kwargs):
self.header = kwargs['header']
self.files = kwargs['files']
self.clips = kwargs['clips']
self.plugins = kwargs['plugins']
self.tracks = kwargs['tracks']
self.markers = kwargs['markers']
def markers_timed(self,
only_ruler_markers: bool = True) -> \
Iterator[Tuple['MarkerDescriptor', Fraction]]:
"""
Iterate each marker in the session with its respective time reference.
"""
for marker in self.markers:
if marker.track_marker and only_ruler_markers:
continue
marker_time = Fraction(marker.time_reference,
int(self.header.sample_rate))
# marker_time = self.header.convert_timecode(marker.location)
yield marker, marker_time
def tracks_clips(self) -> Iterator[Tuple['TrackDescriptor',
'TrackClipDescriptor']]:
"""
Iterate each track clip with its respective owning clip.
"""
for track in self.tracks:
for clip in track.clips:
yield track, clip
def track_clips_timed(self) -> Iterator[Tuple["TrackDescriptor",
"TrackClipDescriptor",
Fraction, Fraction, Fraction]
]:
"""
Iterate each track clip with its respective owning clip and timing
information.
:returns: A Generator that yields track, clip, start time, finish time,
and timestamp
"""
for track, clip in self.tracks_clips():
start_time = self.header.convert_timecode(clip.start_timecode)
finish_time = self.header.convert_timecode(clip.finish_timecode)
timestamp_time = self.header.convert_timecode(clip.timestamp) \
if clip.timestamp is not None else None
yield track, clip, start_time, finish_time, timestamp_time
class HeaderDescriptor:
session_name: str
sample_rate: float
bit_depth: int
start_timecode: str
timecode_fps: str
timecode_drop_frame: bool
count_audio_tracks: int
count_clips: int
count_files: int
def __init__(self, **kwargs):
self.session_name = kwargs['session_name']
self.sample_rate = kwargs['sample_rate']
self.bit_depth = kwargs['bit_depth']
self.start_timecode = kwargs['start_timecode']
self.timecode_fps = kwargs['timecode_format']
self.timecode_drop_frame = kwargs['timecode_drop_frame']
self.count_audio_tracks = kwargs['count_audio_tracks']
self.count_clips = kwargs['count_clips']
self.count_files = kwargs['count_files']
@property
def timecode_format(self):
return TimecodeFormat(frame_duration=self.frame_duration,
logical_fps=self.logical_fps,
drop_frame=self.timecode_drop_frame)
def convert_timecode(self, tc_string: str) -> Fraction:
return self.timecode_format.smpte_to_seconds(tc_string)
@property
def start_time(self) -> Fraction:
"""
The start time of this session.
:return: Start time in seconds
"""
return self.convert_timecode(self.start_timecode)
@property
def logical_fps(self) -> int:
return self._get_tc_format_params[0]
@property
def frame_duration(self) -> Fraction:
return self._get_tc_format_params[1]
@property
def _get_tc_format_params(self) -> Tuple[int, Fraction]:
frame_rates = {"23.976": (24, Fraction(1001, 24_000)),
"24": (24, Fraction(1, 24)),
"25": (25, Fraction(1, 25)),
"29.97": (30, Fraction(1001, 30_000)),
"30": (30, Fraction(1, 30)),
"59.94": (60, Fraction(1001, 60_000)),
"60": (60, Fraction(1, 60))
}
if self.timecode_fps in frame_rates.keys():
return frame_rates[self.timecode_fps]
else:
raise ValueError("Unrecognized TC rate (%s)" %
self.timecode_format)
class TrackDescriptor:
index: int
name: str
comments: str
user_delay_samples: int
state: List[str]
plugins: List[str]
clips: List["TrackClipDescriptor"]
def __init__(self, **kwargs):
self.index = kwargs['index']
self.name = kwargs['name']
self.comments = kwargs['comments']
self.user_delay_samples = kwargs['user_delay_samples']
self.state = kwargs['state']
self.plugins = kwargs['plugins']
self.clips = kwargs['clips']
class FileDescriptor(dict):
pass
class TrackClipDescriptor:
channel: int
event: int
clip_name: str
start_timecode: str
finish_timecode: str
duration: str
timestamp: str
state: str
def __init__(self, **kwargs):
self.channel = kwargs['channel']
self.event = kwargs['event']
self.clip_name = kwargs['clip_name']
self.start_timecode = kwargs['start_time']
self.finish_timecode = kwargs['finish_time']
self.duration = kwargs['duration']
self.timestamp = kwargs['timestamp']
self.state = kwargs['state']
class ClipDescriptor(dict):
pass
class PluginDescriptor(dict):
pass
class MarkerDescriptor:
number: int
location: str
time_reference: int
units: str
name: str
comments: str
track_marker: bool
def __init__(self, **kwargs):
self.number = kwargs['number']
self.location = kwargs['location']
self.time_reference = kwargs['time_reference']
self.units = kwargs['units']
self.name = kwargs['name']
self.comments = kwargs['comments']
self.track_marker = kwargs['track_marker']
-58
View File
@@ -1,58 +0,0 @@
from fractions import Fraction
from typing import Tuple, List
from reportlab.lib.pagesizes import portrait, letter
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import Paragraph, Table
from ptulsconv.broadcast_timecode import TimecodeFormat
from ptulsconv.pdf import make_doc_template
# TODO: A Continuity
def table_for_scene(scene, tc_format, font_name='Helvetica'):
scene_style = getSampleStyleSheet()['Normal']
scene_style.fontName = font_name
scene_style.leftIndent = 0.
scene_style.leftPadding = 0.
scene_style.spaceAfter = 18.
tc_data = "<em>%s</em><br />%s" % (tc_format.seconds_to_smpte(scene[2]),
tc_format.seconds_to_smpte(scene[3]))
row = [
Paragraph(tc_data, scene_style),
Paragraph(scene[1], scene_style),
]
style = [('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (0, 0), 0.0),
('BOTTOMPADDING', (0, 0), (-1, -1), 12.),
('FONTNAME', (0, 0), (-1, -1), font_name)]
return Table(data=[row], style=style, colWidths=[1.0 * inch, 6.5 * inch])
def output_report(scenes: List[Tuple[str, str, Fraction, Fraction]],
tc_display_format: TimecodeFormat,
title: str, client: str, supervisor, paper_size=letter):
filename = "%s Continuity.pdf" % title
document_header = "Continuity"
doc = make_doc_template(page_size=portrait(paper_size),
filename=filename,
document_title="Continuity",
title=title,
client=client,
document_subheader="",
supervisor=supervisor,
document_header=document_header,
left_margin=0.5 * inch)
story = list()
# story.append(Spacer(height=0.5 * inch, width=1.))
for scene in scenes:
story.append(table_for_scene(scene, tc_display_format))
doc.build(story)
-304
View File
@@ -1,304 +0,0 @@
from typing import List, Optional
# from reportlab.pdfbase import pdfmetrics
# from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.units import inch
from reportlab.lib.pagesizes import letter, portrait
from reportlab.lib import colors
from reportlab.platypus import Table, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet
from .__init__ import time_format, make_doc_template
from ..docparser.adr_entity import ADRLine
def build_columns(lines: List[ADRLine], reel_list: Optional[List[str]],
show_priorities=False, include_omitted=False):
columns = list()
reel_numbers = reel_list or sorted(
set([x.reel for x in lines if x.reel is not None])
)
num_column_width = 15. / 32. * inch
columns.append({
'heading': '#',
'value_getter': lambda recs: recs[0].character_id,
'value_getter2': lambda recs: "",
'style_getter': lambda col_index: [],
'width': 0.375 * inch,
'summarize': False
})
columns.append({
'heading': 'Role',
'value_getter': lambda recs: recs[0].character_name,
'value_getter2': lambda recs: recs[0].actor_name or "",
'style_getter': lambda col_index: [('LINEAFTER',
(col_index, 0),
(col_index, -1),
1.0, colors.black)],
'width': 1.75 * inch,
'summarize': False
})
columns.append({
'heading': 'TV',
'value_getter': lambda recs: len([r for r in recs if r.tv]),
'value_getter2': (lambda recs:
time_format(sum([r.time_budget_mins or 0.
for r in recs if r.tv]))
),
'style_getter': (lambda col_index:
[('ALIGN', (col_index, 0), (col_index, -1),
'CENTER'),
('LINEBEFORE', (col_index, 0), (col_index, -1),
1., colors.black),
('LINEAFTER', (col_index, 0), (col_index, -1),
.5, colors.gray)]
),
'width': num_column_width
})
columns.append({
'heading': 'Opt',
'value_getter': lambda recs: len([r for r in recs if r.optional]),
'value_getter2': (lambda recs:
time_format(sum([r.time_budget_mins or 0.
for r in recs if r.optional]))
),
'style_getter': (lambda col_index:
[('ALIGN', (col_index, 0), (col_index, -1),
'CENTER'),
('LINEAFTER', (col_index, 0), (col_index, -1),
.5, colors.gray)]
),
'width': num_column_width
})
columns.append({
'heading': 'Eff',
'value_getter': lambda recs: len([r for r in recs if r.effort]),
'value_getter2': (lambda recs:
time_format(sum([r.time_budget_mins or 0.
for r in recs if r.effort]))
),
'style_getter': (lambda col_index:
[('ALIGN', (col_index, 0), (col_index, -1),
'CENTER')]
),
'width': num_column_width
})
columns.append({
'heading': '',
'value_getter': lambda _: '',
'value_getter2': lambda _: '',
'style_getter': lambda col_index: [
('LINEBEFORE', (col_index, 0), (col_index, -1), 1., colors.black),
('LINEAFTER', (col_index, 0), (col_index, -1), 1., colors.black),
],
'width': 2.
})
if len(reel_numbers) > 0:
for n in reel_numbers:
columns.append({
'heading': n,
'value_getter': (lambda recs, n1=n:
len([r for r in recs if r.reel == n1])
),
'value_getter2': (lambda recs, n1=n:
time_format(sum([r.time_budget_mins or 0.
for r in recs
if r.reel == n1]))
),
'style_getter': (lambda col_index:
[('ALIGN', (col_index, 0), (col_index, -1),
'CENTER'),
('LINEAFTER', (col_index, 0),
(col_index, -1),
.5, colors.gray)]
),
'width': num_column_width
})
if show_priorities:
for n in range(1, 6,):
columns.append({
'heading': 'P%i' % n,
'value_getter': lambda recs: len([r for r in recs
if r.priority == n]),
'value_getter2': (lambda recs:
time_format(sum([r.time_budget_mins or 0.
for r in recs
if r.priority == n]))
),
'style_getter': lambda col_index: [],
'width': num_column_width
})
columns.append({
'heading': '>P5',
'value_getter': lambda recs: len([r for r in recs
if (r.priority or 5) > 5]),
'value_getter2': (lambda recs:
time_format(sum([r.time_budget_mins or 0.
for r in recs
if (r.priority or 5) > 5]))
),
'style_getter': lambda col_index: [],
'width': num_column_width
})
if include_omitted:
columns.append({
'heading': 'Omit',
'value_getter': lambda recs: len([r for r in recs if r.omitted]),
'value_getter2': (lambda recs:
time_format(sum([r.time_budget_mins or 0.
for r in recs if r.omitted]))),
'style_getter': (lambda col_index:
[('ALIGN', (col_index, 0), (col_index, -1),
'CENTER')]
),
'width': num_column_width
})
columns.append({
'heading': 'Total',
'value_getter': lambda recs: len([r for r in recs if not r.omitted]),
'value_getter2': (lambda recs:
time_format(
sum([r.time_budget_mins or 0.
for r in recs if not r.omitted])
)
),
'style_getter': (lambda col_index:
[('LINEBEFORE', (col_index, 0), (col_index, -1),
1.0, colors.black),
('ALIGN', (col_index, 0), (col_index, -1),
'CENTER')]
),
'width': 0.5 * inch
})
return columns
def populate_columns(lines: List[ADRLine], columns, include_omitted,
_page_size):
data = list()
styles = list()
columns_widths = list()
sorted_character_numbers: List[str] = sorted(
set([x.character_id for x in lines]),
key=lambda x: str(x))
# construct column styles
for i, c in enumerate(columns):
styles.extend(c['style_getter'](i))
columns_widths.append(c['width'])
data.append(list(map(lambda x: x['heading'], columns)))
if not include_omitted:
lines = [x for x in lines if not x.omitted]
for n in sorted_character_numbers:
char_records = [x for x in lines if x.character_id == n]
if len(char_records) > 0:
row_data = list()
row_data2 = list()
for col in columns:
row1_index = len(data)
row2_index = row1_index + 1
row_data.append(col['value_getter'](list(char_records)))
row_data2.append(col['value_getter2'](list(char_records)))
styles.extend([('TEXTCOLOR', (0, row2_index), (-1, row2_index),
colors.red),
('LINEBELOW', (0, row2_index), (-1, row2_index),
0.5, colors.black)])
data.append(row_data)
data.append(row_data2)
summary_row1 = list()
summary_row2 = list()
row1_index = len(data)
for col in columns:
if col.get('summarize', True):
summary_row1.append(col['value_getter'](lines))
summary_row2.append(col['value_getter2'](lines))
else:
summary_row1.append("")
summary_row2.append("")
styles.append(('LINEABOVE', (0, row1_index), (-1, row1_index), 2.0,
colors.black))
data.append(summary_row1)
data.append(summary_row2)
return data, styles, columns_widths
# def build_header(column_widths):
# pass
def output_report(lines: List[ADRLine], reel_list: List[str],
include_omitted=False, page_size=portrait(letter),
font_name='Helvetica'):
columns = build_columns(lines, include_omitted=include_omitted,
reel_list=reel_list)
data, style, columns_widths = populate_columns(lines, columns,
include_omitted, page_size)
style.append(('FONTNAME', (0, 0), (-1, -1), font_name))
style.append(('FONTSIZE', (0, 0), (-1, -1), 9.))
style.append(('LINEBELOW', (0, 0), (-1, 0), 1.0, colors.black))
# style.append(('LINEBELOW', (0, 1), (-1, -1), 0.25, colors.gray))
# pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
title = "%s Line Count" % lines[0].title
filename = title + '.pdf'
doc = make_doc_template(page_size=page_size, filename=filename,
document_title=title, title=lines[0].title,
document_subheader=lines[0].spot,
client=lines[0].client,
supervisor=lines[0].supervisor,
document_header='Line Count')
# header_data, header_style, header_widths = build_header(columns_widths)
# header_table = Table(data=header_data, style=header_style,
# colWidths=header_widths)
table = Table(data=data, style=style, colWidths=columns_widths)
story = [Spacer(height=0.5 * inch, width=1.), table]
style = getSampleStyleSheet()['Normal']
style.fontName = font_name
style.fontSize = 12.
style.spaceBefore = 16.
style.spaceAfter = 16.
omitted_count = len([x for x in lines if x.omitted])
if not include_omitted and omitted_count > 0:
story.append(Paragraph("* %i Omitted lines are excluded." %
omitted_count, style))
doc.build(story)
-157
View File
@@ -1,157 +0,0 @@
# -*- coding: utf-8 -*-
from .__init__ import time_format, make_doc_template
from reportlab.lib.units import inch
from reportlab.lib.pagesizes import letter, portrait
from reportlab.platypus import Paragraph, Spacer, KeepTogether, Table
from reportlab.lib.styles import getSampleStyleSheet
from typing import List
from ptulsconv.docparser.adr_entity import ADRLine
from ptulsconv.broadcast_timecode import TimecodeFormat
def build_aux_data_field(line: ADRLine):
entries = list()
if line.reason is not None:
entries.append("Reason: " + line.reason)
if line.note is not None:
entries.append("Note: " + line.note)
if line.requested_by is not None:
entries.append("Requested by: " + line.requested_by)
if line.shot is not None:
entries.append("Shot: " + line.shot)
fg_color = 'white'
tag_field = ""
if line.effort:
bg_color = 'red'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " \
% (bg_color, fg_color, "EFF")
elif line.tv:
bg_color = 'blue'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " \
% (bg_color, fg_color, "TV")
elif line.adlib:
bg_color = 'purple'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " \
% (bg_color, fg_color, "ADLIB")
elif line.optional:
bg_color = 'green'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font>" \
% (bg_color, fg_color, "OPTIONAL")
entries.append(tag_field)
return "<br />".join(entries)
def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat,
font_name='Helvetica'):
story = list()
this_scene = None
scene_style = getSampleStyleSheet()['Normal']
scene_style.fontName = font_name
scene_style.leftIndent = 0.
scene_style.leftPadding = 0.
scene_style.spaceAfter = 18.
line_style = getSampleStyleSheet()['Normal']
line_style.fontName = font_name
for line in lines:
table_style = [('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (0, 0), 0.0),
('BOTTOMPADDING', (0, 0), (-1, -1), 24.)]
cue_number_field = "%s<br /><font fontSize=7>%s</font>" \
% (line.cue_number, line.character_name)
time_data = time_format(line.time_budget_mins)
if line.priority is not None:
time_data = time_data + "<br />" + "P: " + line.priority
aux_data_field = build_aux_data_field(line)
tc_data = build_tc_data(line, tc_rate)
line_table_data = [[Paragraph(cue_number_field, line_style),
Paragraph(tc_data, line_style),
Paragraph(line.prompt, line_style),
Paragraph(time_data, line_style),
Paragraph(aux_data_field, line_style)
]]
line_table = Table(data=line_table_data,
colWidths=[inch * 0.75, inch, inch * 3., 0.5 * inch,
inch * 2.],
style=table_style)
if (line.scene or "[No Scene]") != this_scene:
this_scene = line.scene or "[No Scene]"
story.append(KeepTogether([
Spacer(1., 0.25 * inch),
Paragraph("<u>" + this_scene + "</u>", scene_style),
line_table]))
else:
line_table.setStyle(table_style)
story.append(KeepTogether([line_table]))
return story
def build_tc_data(line: ADRLine, tc_format: TimecodeFormat):
tc_data = tc_format.seconds_to_smpte(line.start) + "<br />" + \
tc_format.seconds_to_smpte(line.finish)
third_line = []
if line.reel is not None:
if line.reel[0:1] == 'R':
third_line.append("%s" % line.reel)
else:
third_line.append("Reel %s" % line.reel)
if line.version is not None:
third_line.append("(%s)" % line.version)
if len(third_line) > 0:
tc_data = tc_data + "<br/>" + " ".join(third_line)
return tc_data
def generate_report(page_size, lines: List[ADRLine], tc_rate: TimecodeFormat,
character_number=None, include_omitted=True):
if character_number is not None:
lines = [r for r in lines if r.character_id == character_number]
title = "%s ADR Report (%s)" % (lines[0].title,
lines[0].character_name)
document_header = "%s ADR Report" % lines[0].character_name
else:
title = "%s ADR Report" % lines[0].title
document_header = 'ADR Report'
if not include_omitted:
lines = [line for line in lines if not line.omitted]
lines = sorted(lines, key=lambda line: line.start)
filename = title + ".pdf"
doc = make_doc_template(page_size=page_size,
filename=filename, document_title=title,
document_header=document_header,
title=lines[0].title,
supervisor=lines[0].supervisor,
client=lines[0].client,
document_subheader=lines[0].spot,
left_margin=0.75 * inch)
story = build_story(lines, tc_rate)
doc.build(story)
def output_report(lines: List[ADRLine], tc_display_format: TimecodeFormat,
page_size=portrait(letter), by_character=False):
if by_character:
character_numbers = set((r.character_id for r in lines))
for n in character_numbers:
generate_report(page_size, lines, tc_display_format, n)
else:
generate_report(page_size, lines, tc_display_format)
-293
View File
@@ -1,293 +0,0 @@
from reportlab.pdfgen.canvas import Canvas
# from reportlab.pdfbase import pdfmetrics
# from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.units import inch
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Paragraph
from .__init__ import GRect
from ptulsconv.broadcast_timecode import TimecodeFormat
from ptulsconv.docparser.adr_entity import ADRLine
import datetime
font_name = 'Helvetica'
def draw_header_block(canvas, rect, record: ADRLine):
rect.draw_text_cell(canvas, record.cue_number, "Helvetica", 44,
vertical_align='m')
def draw_character_row(canvas, rect, record: ADRLine):
label_frame, value_frame = rect.split_x(1.25 * inch)
label_frame.draw_text_cell(canvas, "CHARACTER", font_name, 10,
force_baseline=9.)
line = "%s / %s" % (record.character_id, record.character_name)
if record.actor_name is not None:
line = line + " / " + record.actor_name
value_frame.draw_text_cell(canvas, line, font_name, 12, force_baseline=9.)
rect.draw_border(canvas, ['min_y', 'max_y'])
def draw_cue_number_block(canvas, rect, record: ADRLine):
(label_frame, number_frame,), aux_frame = \
rect.divide_y([0.20 * inch, 0.375 * inch], direction='d')
label_frame.draw_text_cell(canvas, "CUE NUMBER", font_name, 10,
inset_y=5., vertical_align='t')
number_frame.draw_text_cell(canvas, record.cue_number, font_name, 14,
inset_x=10., inset_y=2., draw_baseline=True)
tags = {'tv': 'TV',
'optional': 'OPT',
'adlib': 'ADLIB',
'effort': 'EFF',
'tbw': 'TBW',
'omitted': 'OMIT'}
tag_field = ""
for key in tags.keys():
if getattr(record, key):
tag_field = tag_field + tags[key] + " "
aux_frame.draw_text_cell(canvas, tag_field, font_name, 10,
inset_x=10., inset_y=2., vertical_align='t')
rect.draw_border(canvas, 'max_x')
def draw_timecode_block(canvas, rect, record: ADRLine,
tc_display_format: TimecodeFormat):
(in_label_frame, in_frame, out_label_frame, out_frame), _ = rect.divide_y(
[0.20 * inch, 0.25 * inch, 0.20 * inch, 0.25 * inch], direction='d')
in_label_frame.draw_text_cell(canvas, "IN", font_name, 10,
vertical_align='t', inset_y=5., inset_x=5.)
in_frame.draw_text_cell(canvas,
tc_display_format.seconds_to_smpte(record.start),
font_name, 14,
inset_x=10., inset_y=2.,
draw_baseline=True)
out_label_frame.draw_text_cell(canvas, "OUT", font_name, 10,
vertical_align='t', inset_y=5., inset_x=5.)
out_frame.draw_text_cell(canvas,
tc_display_format.seconds_to_smpte(record.finish),
font_name, 14,
inset_x=10., inset_y=2.,
draw_baseline=True)
rect.draw_border(canvas, 'max_x')
def draw_reason_block(canvas, rect, record: ADRLine):
reason_cell, notes_cell = rect.split_y(24., direction='d')
reason_label, reason_value = reason_cell.split_x(.75 * inch)
notes_label, notes_value = notes_cell.split_x(.75 * inch)
reason_label.draw_text_cell(canvas, "Reason:", font_name, 12,
inset_x=5., inset_y=5., vertical_align='b')
reason_value.draw_text_cell(canvas, record.reason or "", font_name, 12,
inset_x=5., inset_y=5., draw_baseline=True,
vertical_align='b')
notes_label.draw_text_cell(canvas, "Note:", font_name, 12,
inset_x=5., inset_y=5., vertical_align='t')
style = getSampleStyleSheet()['BodyText']
style.fontName = font_name
style.fontSize = 12
style.leading = 14
p = Paragraph(record.note or "", style)
notes_value.draw_flowable(canvas, p, draw_baselines=True,
inset_x=5., inset_y=5.)
def draw_prompt(canvas, rect, prompt=""):
label, block = rect.split_y(0.20 * inch, direction='d')
label.draw_text_cell(canvas, "PROMPT", font_name, 10, vertical_align='t',
inset_y=5., inset_x=0.)
style = getSampleStyleSheet()['BodyText']
style.fontName = font_name
style.fontSize = 14
style.leading = 24
style.leftIndent = 1.5 * inch
style.rightIndent = 1.5 * inch
p = Paragraph(prompt, style)
block.draw_flowable(canvas, p, draw_baselines=True)
rect.draw_border(canvas, 'max_y')
def draw_notes(canvas, rect, note=""):
label, block = rect.split_y(0.20 * inch, direction='d')
label.draw_text_cell(canvas, "NOTES", font_name, 10, vertical_align='t',
inset_y=5., inset_x=0.)
style = getSampleStyleSheet()['BodyText']
style.fontName = font_name
style.fontSize = 14
style.leading = 24
prompt = Paragraph(note, style)
block.draw_flowable(canvas, prompt, draw_baselines=True)
rect.draw_border(canvas, ['max_y', 'min_y'])
def draw_take_grid(canvas, rect):
canvas.saveState()
cp = canvas.beginPath()
cp.rect(rect.min_x, rect.min_y, rect.width, rect.height)
canvas.clipPath(cp, stroke=0, fill=0)
canvas.setDash([3.0, 2.0])
for xi in range(1, 10):
x = xi * (rect.width / 10)
if xi % 5 == 0:
canvas.setDash(1, 0)
else:
canvas.setDash([2, 5])
ln = canvas.beginPath()
ln.moveTo(rect.min_x + x, rect.min_y)
ln.lineTo(rect.min_x + x, rect.max_y)
canvas.drawPath(ln)
for yi in range(1, 10):
y = yi * (rect.height / 6)
if yi % 2 == 0:
canvas.setDash(1, 0)
else:
canvas.setDash([2, 5])
ln = canvas.beginPath()
ln.moveTo(rect.min_x, rect.min_y + y)
ln.lineTo(rect.max_x, rect.min_y + y)
canvas.drawPath(ln)
rect.draw_border(canvas, 'max_x')
canvas.restoreState()
def draw_aux_block(canvas, rect, recording_time_sec_this_line,
recording_time_sec):
rect.draw_border(canvas, 'min_x')
content_rect = rect.inset_xy(10., 10.)
lines, last_line = content_rect.divide_y([12., 12., 24., 24., 24., 24.],
direction='d')
lines[0].draw_text_cell(canvas,
"Time for this line: %.1f mins" %
(recording_time_sec_this_line / 60.),
font_name, 9.)
lines[1].draw_text_cell(canvas, "Running time: %03.1f mins" %
(recording_time_sec / 60.), font_name, 9.)
lines[2].draw_text_cell(canvas, "Actual Start: ______________",
font_name, 9., vertical_align='b')
lines[3].draw_text_cell(canvas, "Record Date: ______________",
font_name, 9., vertical_align='b')
lines[4].draw_text_cell(canvas, "Engineer: ______________",
font_name, 9., vertical_align='b')
lines[5].draw_text_cell(canvas, "Location: ______________",
font_name, 9., vertical_align='b')
def draw_footer(canvas, rect, record: ADRLine, report_date, line_no,
total_lines):
rect.draw_border(canvas, 'max_y')
report_date_s = [report_date.strftime("%c")]
spotting_name = [record.spot] if record.spot is not None else []
pages_s = ["Line %i of %i" % (line_no, total_lines)]
footer_s = " - ".join(report_date_s + spotting_name + pages_s)
rect.draw_text_cell(canvas, footer_s, font_name=font_name, font_size=10.,
inset_y=2.)
def create_report_for_character(records, report_date,
tc_display_format: TimecodeFormat):
outfile = "%s_%s_%s_Log.pdf" % (records[0].title,
records[0].character_id,
records[0].character_name,)
assert outfile is not None
assert outfile[-4:] == '.pdf', "Output file must have 'pdf' extension!"
# pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
page: GRect = GRect(0, 0, letter[0], letter[1])
page = page.inset(inch * 0.5)
(header_row, char_row, data_row,
prompt_row, notes_row, takes_row), footer = \
page.divide_y([0.875 * inch, 0.375 * inch, inch,
3.0 * inch, 1.5 * inch, 3 * inch], direction='d')
cue_header_block, title_header_block = header_row.split_x(4.0 * inch)
(cue_number_block, timecode_block), reason_block = \
data_row.divide_x([1.5 * inch, 1.5 * inch])
(take_grid_block), aux_block = takes_row.split_x(5.25 * inch)
c = Canvas(outfile, pagesize=letter,)
c.setTitle("%s %s (%s) Supervisor's Log" % (records[0].title,
records[0].character_name,
records[0].character_id))
c.setAuthor(records[0].supervisor)
recording_time_sec = 0.0
total_lines = len(records)
line_n = 1
for record in records:
record: ADRLine
recording_time_sec_this_line: float = (
record.time_budget_mins or 6.0) * 60.0
recording_time_sec = recording_time_sec + recording_time_sec_this_line
draw_header_block(c, cue_header_block, record)
# FIXME: Draw the title
# TODO: Integrate this report into the common DocTemplate api
# draw_title_box(c, title_header_block, record)
draw_character_row(c, char_row, record)
draw_cue_number_block(c, cue_number_block, record)
draw_timecode_block(c, timecode_block, record,
tc_display_format=tc_display_format)
draw_reason_block(c, reason_block, record)
draw_prompt(c, prompt_row, prompt=record.prompt or "")
draw_notes(c, notes_row, note="")
draw_take_grid(c, take_grid_block)
draw_aux_block(c, aux_block, recording_time_sec_this_line,
recording_time_sec)
draw_footer(c, footer, record, report_date, line_no=line_n,
total_lines=total_lines)
line_n = line_n + 1
c.showPage()
c.save()
def output_report(lines, tc_display_format: TimecodeFormat):
report_date = datetime.datetime.now()
events = sorted(lines, key=lambda x: x.start)
character_numbers = set([x.character_id for x in lines])
for n in character_numbers:
create_report_for_character([e for e in events if e.character_id == n],
report_date,
tc_display_format=tc_display_format)
-83
View File
@@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
from typing import List
from .__init__ import make_doc_template
from reportlab.lib.units import inch
from reportlab.lib.pagesizes import letter
from reportlab.platypus import Paragraph, Spacer, KeepTogether, Table, \
HRFlowable
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors
# from reportlab.pdfbase import pdfmetrics
# from reportlab.pdfbase.ttfonts import TTFont
from ..broadcast_timecode import TimecodeFormat
from ..docparser.adr_entity import ADRLine
def output_report(lines: List[ADRLine], tc_display_format: TimecodeFormat,
font_name="Helvetica"):
character_numbers = set([n.character_id for n in lines])
# pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
for n in character_numbers:
char_lines = [line for line in lines
if not line.omitted and line.character_id == n]
character_name = char_lines[0].character_name
char_lines = sorted(char_lines, key=lambda line: line.start)
title = "%s (%s) %s ADR Script" % (char_lines[0].title,
character_name, n)
filename = "%s_%s_%s_ADR Script.pdf" % (char_lines[0].title,
n, character_name)
doc = make_doc_template(page_size=letter, filename=filename,
document_title=title,
title=char_lines[0].title,
document_subheader=char_lines[0].spot or "",
supervisor=char_lines[0].supervisor or "",
client=char_lines[0].client or "",
document_header=character_name or "")
story = []
prompt_style = getSampleStyleSheet()['Normal']
prompt_style.fontName = font_name
prompt_style.fontSize = 18.
prompt_style.leading = 24.
prompt_style.leftIndent = 1.5 * inch
prompt_style.rightIndent = 1.5 * inch
number_style = getSampleStyleSheet()['Normal']
number_style.fontName = font_name
number_style.fontSize = 14
number_style.leading = 24
number_style.leftIndent = 0.
number_style.rightIndent = 0.
for line in char_lines:
start_tc = tc_display_format.seconds_to_smpte(line.start)
finish_tc = tc_display_format.seconds_to_smpte(line.finish)
data_block = [[Paragraph(line.cue_number, number_style),
Paragraph(start_tc + " - " + finish_tc,
number_style)
]]
# RIGHTWARDS ARROW →
# Unicode: U+2192, UTF-8: E2 86 92
story.append(
KeepTogether(
[HRFlowable(width='50%', color=colors.black),
Table(data=data_block, colWidths=[1.5 * inch, 6. * inch],
style=[('LEFTPADDING', (0, 0), (-1, -1), 0.)]),
Paragraph(line.prompt, prompt_style),
Spacer(1., inch * 1.5)]
)
)
doc.build(story)
-71
View File
@@ -1,71 +0,0 @@
"""
Reporting logic. These methods provide reporting methods to the package and
take some pains to provide nice-looking escape codes if we're writing to a
tty.
"""
import sys
def print_banner_style(message):
if sys.stderr.isatty():
sys.stderr.write("\n\033[1m%s\033[0m\n\n" % message)
else:
sys.stderr.write("\n%s\n\n" % message)
def print_section_header_style(message):
if sys.stderr.isatty():
sys.stderr.write("\n\033[4m%s\033[0m\n\n" % message)
else:
sys.stderr.write("%s\n\n" % message)
def print_status_style(message):
if sys.stderr.isatty():
sys.stderr.write("\033[3m - %s\033[0m\n" % message)
else:
sys.stderr.write(" - %s\n" % message)
def print_warning(warning_string):
if sys.stderr.isatty():
sys.stderr.write("\033[3m - %s\033[0m\n" % warning_string)
else:
sys.stderr.write(" - %s\n" % warning_string)
def print_advisory_tagging_error(failed_string, position,
parent_track_name=None, clip_time=None):
if sys.stderr.isatty():
sys.stderr.write("\n")
sys.stderr.write(" ! \033[33;1mTagging error: \033[0m")
ok_string = failed_string[:position]
not_ok_string = failed_string[position:]
sys.stderr.write("\033[32m\"%s\033[31;1m%s\"\033[0m\n" %
(ok_string, not_ok_string))
if parent_track_name is not None:
sys.stderr.write(" ! > On track \"%s\"\n" % parent_track_name)
if clip_time is not None:
sys.stderr.write(" ! > In clip name at %s\n" % clip_time)
else:
sys.stderr.write("\n")
sys.stderr.write(" ! Tagging error: \"%s\"\n" % failed_string)
sys.stderr.write(" ! %s _______________⬆\n" % (" " * position))
if parent_track_name is not None:
sys.stderr.write(" ! > On track \"%s\"\n" % parent_track_name)
if clip_time is not None:
sys.stderr.write(" ! > In clip name at %s\n" % clip_time)
sys.stderr.write("\n")
def print_fatal_error(message):
if sys.stderr.isatty():
sys.stderr.write("\n\033[5;31;1m*** %s ***\033[0m\n" % message)
else:
sys.stderr.write("\n%s\n" % message)
-85
View File
@@ -1,85 +0,0 @@
"""
Validation logic for enforcing various consistency rules.
"""
from dataclasses import dataclass
from ptulsconv.docparser.adr_entity import ADRLine
from typing import Iterator, Optional
@dataclass
class ValidationError:
message: str
event: Optional[ADRLine] = None
def report_message(self):
if self.event is not None:
return (f"{self.message}: event at {self.event.start} with number"
"{self.event.cue_number}")
else:
return self.message
def validate_unique_count(input_lines: Iterator[ADRLine], field='title',
count=1):
values = set(list(map(lambda e: getattr(e, field), input_lines)))
if len(values) > count:
yield ValidationError(
message="Field {} has too many values (max={}): {}"
.format(field, count, values)
)
def validate_value(input_lines: Iterator[ADRLine], key_field, predicate):
for event in input_lines:
val = getattr(event, key_field)
if not predicate(val):
yield ValidationError(message='Field {} not in range'.format(val),
event=event)
def validate_unique_field(input_lines: Iterator[ADRLine], field='cue_number',
scope=None):
values = dict()
for event in input_lines:
this = getattr(event, field)
if scope is not None:
key = getattr(event, scope)
else:
key = '_values'
values.setdefault(key, set())
if this in values[key]:
yield ValidationError(message='Re-used {}'.format(field),
event=event)
else:
values[key].update(this)
def validate_non_empty_field(input_lines: Iterator[ADRLine],
field='cue_number'):
for event in input_lines:
if getattr(event, field, None) is None:
yield ValidationError(message='Empty field {}'.format(field),
event=event)
def validate_dependent_value(input_lines: Iterator[ADRLine], key_field,
dependent_field):
"""
Validates that two events with the same value in `key_field` always have
the same value in `dependent_field`
"""
key_values = set((getattr(x, key_field) for x in input_lines))
for key_value in key_values:
rows = [(getattr(x, key_field), getattr(x, dependent_field))
for x in input_lines
if getattr(x, key_field) == key_value]
unique_rows = set(rows)
if len(unique_rows) > 1:
message = "Non-unique values for key {} = ".format(key_field)
for u in unique_rows:
message = message + "\n - {} -> {}".format(u[0], u[1])
yield ValidationError(message=message, event=None)
-183
View File
@@ -1,183 +0,0 @@
import os
import os.path
import pathlib
import subprocess
import sys
import glob
import datetime
from xml.etree.ElementTree import TreeBuilder, tostring
from typing import List
import ptulsconv
from ptulsconv.docparser.adr_entity import ADRLine
# TODO Get a third-party test for Avid Marker lists
def avid_marker_list(lines: List[ADRLine], report_date=datetime.datetime.now(),
reel_start_frame=0, fps=24):
doc = TreeBuilder(element_factory=None)
doc.start('Avid:StreamItems', {'xmlns:Avid': 'http://www.avid.com'})
doc.start('Avid:XMLFileData', {})
doc.start('AvProp', {'name': 'DomainMagic', 'type': 'string'})
doc.data("Domain")
doc.end('AvProp')
doc.start('AvProp', {'name': 'DomainKey', 'type': 'string'})
doc.data("58424a44")
doc.end('AvProp')
def insert_elem(kind, attb, atype, name, value):
doc.start('ListElem', {})
doc.start('AvProp', {'id': 'ATTR',
'name': 'OMFI:ATTB:Kind',
'type': 'int32'})
doc.data(kind)
doc.end('AvProp')
doc.start('AvProp', {'id': 'ATTR',
'name': 'OMFI:ATTB:Name',
'type': 'string'})
doc.data(name)
doc.end('AvProp')
doc.start('AvProp', {'id': 'ATTR',
'name': attb,
'type': atype})
doc.data(value)
doc.end('AvProp')
doc.end('ListElem')
for line in lines:
doc.start('AvClass', {'id': 'ATTR'})
doc.start('AvProp', {'id': 'ATTR',
'name': '__OMFI:ATTR:NumItems',
'type': 'int32'})
doc.data('7')
doc.end('AvProp')
doc.start('List', {'id': 'OMFI:ATTR:AttrRefs'})
insert_elem('1', 'OMFI:ATTB:IntAttribute', 'int32',
'_ATN_CRM_LONG_CREATE_DATE', report_date.strftime("%s"))
insert_elem('2', 'OMFI:ATTB:StringAttribute', 'string',
'_ATN_CRM_COLOR', 'yellow')
insert_elem('2', 'OMFI:ATTB:StringAttribute', 'string',
'_ATN_CRM_USER', line.supervisor or "")
marker_name = "%s: %s" % (line.cue_number, line.prompt)
insert_elem('2', 'OMFI:ATTB:StringAttribute', 'string',
'_ATN_CRM_COM', marker_name)
start_frame = int(line.start * fps)
insert_elem('2', "OMFI:ATTB:StringAttribute", 'string',
'_ATN_CRM_TC',
str(start_frame - reel_start_frame))
insert_elem('2', "OMFI:ATTB:StringAttribute", 'string',
'_ATN_CRM_TRK', 'V1')
insert_elem('1', "OMFI:ATTB:IntAttribute", 'int32',
'_ATN_CRM_LENGTH', '1')
doc.start('ListElem', {})
doc.end('ListElem')
doc.end('List')
doc.end('AvClass')
doc.end('Avid:XMLFileData')
doc.end('Avid:StreamItems')
def dump_fmpxml(data, input_file_name, output, adr_field_map):
doc = TreeBuilder(element_factory=None)
doc.start('FMPXMLRESULT', {'xmlns':
'http://www.filemaker.com/fmpxmlresult'})
doc.start('ERRORCODE', {})
doc.data('0')
doc.end('ERRORCODE')
doc.start('PRODUCT', {'NAME': ptulsconv.__name__,
'VERSION': ptulsconv.__version__})
doc.end('PRODUCT')
doc.start('DATABASE', {'DATEFORMAT': 'MM/dd/yy',
'LAYOUT': 'summary',
'TIMEFORMAT': 'hh:mm:ss',
'RECORDS': str(len(data['events'])),
'NAME': os.path.basename(input_file_name)})
doc.end('DATABASE')
doc.start('METADATA', {})
for field in adr_field_map:
tp = field[2]
ft = 'TEXT'
if tp is int or tp is float:
ft = 'NUMBER'
doc.start('FIELD', {'EMPTYOK': 'YES', 'MAXREPEAT': '1',
'NAME': field[1], 'TYPE': ft})
doc.end('FIELD')
doc.end('METADATA')
doc.start('RESULTSET', {'FOUND': str(len(data['events']))})
for event in data['events']:
doc.start('ROW', {})
for field in adr_field_map:
doc.start('COL', {})
doc.start('DATA', {})
for key_attempt in field[0]:
if key_attempt in event.keys():
doc.data(str(event[key_attempt]))
break
doc.end('DATA')
doc.end('COL')
doc.end('ROW')
doc.end('RESULTSET')
doc.end('FMPXMLRESULT')
docelem = doc.close()
xmlstr = tostring(docelem, encoding='unicode', method='xml')
output.write(xmlstr)
xslt_path = os.path.join(pathlib.Path(__file__).parent.absolute(), 'xslt')
def xform_options():
return glob.glob(os.path.join(xslt_path, "*.xsl"))
def dump_xform_options(output=sys.stdout):
print("# Available transforms:", file=output)
print("# Transform dir: %s" % xslt_path, file=output)
for f in xform_options():
base = os.path.basename(f)
name, _ = os.path.splitext(base)
print("# " + name, file=output)
def fmp_transformed_dump(data, input_file, xsl_name, output, adr_field_map):
from ptulsconv.reporting import print_status_style
import io
pipe = io.StringIO()
print_status_style("Generating base XML")
dump_fmpxml(data, input_file, pipe, adr_field_map)
str_data = pipe.getvalue()
print_status_style("Base XML size %i" % (len(str_data)))
print_status_style("Running xsltproc")
xsl_path = os.path.join(pathlib.Path(__file__).parent.absolute(), 'xslt',
xsl_name + ".xsl")
print_status_style("Using xsl: %s" % xsl_path)
subprocess.run(['xsltproc', xsl_path, '-'],
input=str_data, text=True,
stdout=output, shell=False, check=True)
+45 -29
View File
@@ -1,6 +1,13 @@
[project]
name = "ptulsconv"
license = { file = "LICENSE" }
version = "4.0.0"
description = "Read Pro Tools Text exports and generate PDF ADR Reports, JSON"
readme = "README.md"
requires-python = ">=3.9"
license-files = ["LICENSE"]
keywords = ["text-processing", "parsers", "film",
"broadcast", "editing", "editorial"]
classifiers = [
'License :: OSI Approved :: MIT License',
'Topic :: Multimedia',
@@ -13,39 +20,48 @@ classifiers = [
"Development Status :: 5 - Production/Stable",
"Topic :: Text Processing :: Filters"
]
requires-python = ">=3.8"
keywords = ["text-processing", "parsers", "film",
"broadcast", "editing", "editorial"]
[tool.poetry]
name = "ptulsconv"
version = "3.0.0"
description = "Read Pro Tools Text exports and generate PDF ADR Reports, JSON"
authors = ["Jamie Hardt <jamiehardt@me.com>"]
license = "MIT"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.8"
parsimonious = "^0.10.0"
tqdm = "^4.67.1"
reportlab = "^4.4.1"
py-ptsl = "^101.1.0"
sphinx_rtd_theme = {version= '>= 1.1.1', optional=true}
sphinx = {version= '>= 5.3.0', optional=true}
[tool.poetry.extras]
doc = ['sphinx', 'sphinx_rtd_theme']
[tool.poetry.scripts]
ptulsconv = 'ptulsconv.__main__:main'
authors = [{name = "Jamie Hardt", email = "<jamiehardt@me.com>"}]
dependencies = [
"parsimonious>=0.10.0",
"py-ptsl>=600.0.0",
"reportlab>=4.4.4",
"tqdm>=4.67.1",
]
# [tool.uv.build-backend]
# data = { headers = "include/headers", scripts = "bin" }
[project.urls]
Source = 'https://github.com/iluvcapra/ptulsconv'
Issues = 'https://github.com/iluvcapra/ptulsconv/issues'
Documentation = 'https://ptulsconv.readthedocs.io/'
[project.optional-dependencies]
doc = [
"sphinx>=7.4.7",
"sphinx-rtd-theme>=3.0.2",
]
[tool.ruff.lint]
ignore = [
"DTZ005" # datetime created without a tz
]
[tool.ruff.lint.per-file-ignores]
"reaper/Export Items as Text.py" = ["F821"]
[project.scripts]
ptulsconv = "ptulsconv:__main__.main"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
requires = ["uv_build>=0.8.18,<0.12.0"]
build-backend = "uv_build"
[dependency-groups]
dev = [
"ruff>=0.16.8",
]
doc = [
"sphinx>=7.4.7",
"sphinx-rtd-theme>=3.0.2",
]
+45 -21
View File
@@ -1,47 +1,68 @@
# type: ignore[reportUndefinedVariable]
# Export Items as Text.py
# (c) 2021 Jamie Hardt. All rights reserved.
#
#
import json
import os.path
import datetime
import json
from zoneinfo import ZoneInfo
item_records = list()
item_records = []
for i in range(0, RPR_CountMediaItems(0) ):
for i in range(RPR_CountMediaItems(0)):
this_item = RPR_GetMediaItem(0, i)
item_record = {}
item_record["mute"] = True if RPR_GetMediaItemInfo_Value(this_item, "B_MUTE_ACTUAL") > 0. else False
item_record["mute"] = RPR_GetMediaItemInfo_Value(this_item, "B_MUTE_ACTUAL") > 0.0
item_record["duration"] = RPR_GetMediaItemInfo_Value(this_item, "D_LENGTH")
_, item_record["duration_tc"], _, _, _ = RPR_format_timestr_len(item_record["duration"], "", 128, 0., 5)
_, item_record["duration_tc"], _, _, _ = RPR_format_timestr_len(
item_record["duration"], "", 128, 0.0, 5
)
item_record["position"] = RPR_GetMediaItemInfo_Value(this_item, "D_POSITION")
_, item_record["position_tc"], _, _ = RPR_format_timestr_pos(item_record["position"], "", 128, 5)
_, item_record["position_tc"], _, _ = RPR_format_timestr_pos(
item_record["position"], "", 128, 5
)
item_record["selected"] = True if RPR_GetMediaItemInfo_Value(this_item, "B_UISEL") > 0. else False
_, _, _, item_record["notes"], _ = RPR_GetSetMediaItemInfo_String(this_item, "P_NOTES", "", False)
_, _, _, item_record["item_guid"], _ = RPR_GetSetMediaItemInfo_String(this_item, "GUID", "", False)
item_record["selected"] = RPR_GetMediaItemInfo_Value(this_item, "B_UISEL") > 0.0
_, _, _, item_record["notes"], _ = RPR_GetSetMediaItemInfo_String(
this_item, "P_NOTES", "", False
)
_, _, _, item_record["item_guid"], _ = RPR_GetSetMediaItemInfo_String(
this_item, "GUID", "", False
)
active_take = RPR_GetActiveTake(this_item)
_, _, _, item_record["active_take_name"], _ = RPR_GetSetMediaItemTakeInfo_String(active_take, "P_NAME", "", False)
_, _, _, item_record["active_take_guid"], _ = RPR_GetSetMediaItemTakeInfo_String(active_take, "GUID", "", False)
_, _, _, item_record["active_take_name"], _ = RPR_GetSetMediaItemTakeInfo_String(
active_take, "P_NAME", "", False
)
_, _, _, item_record["active_take_guid"], _ = RPR_GetSetMediaItemTakeInfo_String(
active_take, "GUID", "", False
)
item_track = RPR_GetMediaItemTrack(this_item)
_, _, _, item_record["track_name"], _ = RPR_GetSetMediaTrackInfo_String(item_track, "P_NAME", "", False)
_, _, _, item_record["track_guid"], _ = RPR_GetSetMediaTrackInfo_String(item_track, "GUID", "", False)
item_record["track_index"] = RPR_GetMediaTrackInfo_Value(item_track, "IP_TRACKNUMBER")
item_record["track_muted"] = True if RPR_GetMediaTrackInfo_Value(item_track, "B_MUTE") > 0. else False
_, _, _, item_record["track_name"], _ = RPR_GetSetMediaTrackInfo_String(
item_track, "P_NAME", "", False
)
_, _, _, item_record["track_guid"], _ = RPR_GetSetMediaTrackInfo_String(
item_track, "GUID", "", False
)
item_record["track_index"] = RPR_GetMediaTrackInfo_Value(
item_track, "IP_TRACKNUMBER"
)
item_record["track_muted"] = RPR_GetMediaTrackInfo_Value(item_track, "B_MUTE") > 0.0
item_records = item_records + [item_record]
output = dict()
output = {}
output["items"] = item_records
_, output["project_title"], _ = RPR_GetProjectName(0, "", 1024)
_, _, output["project_author"], _ = RPR_GetSetProjectAuthor(0, False, "", 1024)
output["project_frame_rate"], _, output["project_drop_frame"] = RPR_TimeMap_curFrameRate(0, True)
output["project_frame_rate"], _, output["project_drop_frame"] = (
RPR_TimeMap_curFrameRate(0, True)
)
output_path, _ = RPR_GetProjectPath("", 1024)
@@ -51,12 +72,15 @@ output_title = output["project_title"]
if output_title == "":
output_title = "unsaved project"
output_file_name = "%s Text Export %s.txt" % (output_title, now.strftime('%Y%m%d_%H%M'))
output_file_name = f"{output_title} Text Export {now.strftime('%Y%m%d_%H%M')}.txt"
output_path = output_path + "/" + output_file_name
with open(output_path, "w") as f:
json.dump(output, f, allow_nan=True, indent=4)
RPR_ShowMessageBox("Exported text file \"%s\" to project folder." % output_file_name, "Text Export Complete", 0)
RPR_ShowMessageBox(
f'Exported text file "{output_file_name}" to project folder. Text Export Complete',
0,
)
# RPR_ShowConsoleMsg(output_path)
+129
View File
@@ -0,0 +1,129 @@
import datetime
import importlib.metadata
import sys
from optparse import OptionGroup, OptionParser
import ptulsconv
from ptulsconv import __name__
from ptulsconv.commands import convert
from ptulsconv.docparser.adr_entity import ADR_TAG_MAPPING, GENERIC_TAG_MAPPING
from ptulsconv.reporting import (
print_banner_style,
print_fatal_error,
print_section_header_style,
print_status_style,
)
def dump_field_map(output=sys.stdout):
from ptulsconv.docparser.tag_mapping import TagMapping
TagMapping.print_rules(GENERIC_TAG_MAPPING, output=output)
TagMapping.print_rules(ADR_TAG_MAPPING, output=output)
def dump_formats():
print_section_header_style("`raw` format:")
sys.stderr.write("A JSON document of the parsed Pro Tools export.\n")
print_section_header_style("`tagged` Format:")
sys.stderr.write(
"A JSON document containing one record for each clip, with\n"
"all tags parsed and all tagging rules applied. \n"
)
print_section_header_style("`doc` format:")
sys.stderr.write(
"Creates a directory with folders for different types\nof ADR reports.\n\n"
)
def main():
"""Entry point for the command-line invocation"""
parser = OptionParser()
parser.usage = "ptulsconv [options] [TEXT_EXPORT.txt]"
parser.add_option(
"-f",
"--format",
dest="output_format",
metavar="FMT",
choices=["raw", "tagged", "doc"],
default="doc",
help="Set output format, `raw`, `tagged`, `doc`.",
)
parser.add_option(
"-m",
"--movie-opts",
dest="movie_opts",
metavar="MOVIE_OPTS",
help="Set movie options",
)
warn_options = OptionGroup(title="Warning and Validation Options", parser=parser)
warn_options.add_option(
"-W",
action="store_false",
dest="warnings",
default=True,
help="Suppress warnings for common errors (missing code numbers etc.)",
)
parser.add_option_group(warn_options)
informational_options = OptionGroup(
title="Informational Options",
parser=parser,
description="Print useful information and exit without processing input files.",
)
informational_options.add_option(
"--show-formats",
dest="show_formats",
action="store_true",
default=False,
help="Display helpful information about the available output formats.",
)
informational_options.add_option(
"--show-available-tags",
dest="show_tags",
action="store_true",
default=False,
help="Display tag mappings for the FMP XML output style and exit.",
)
parser.add_option_group(informational_options)
version = importlib.metadata.version(ptulsconv.__name__)
print_banner_style(f"{ptulsconv.__name__} - version {version}")
print_banner_style(ptulsconv.__copyright__)
(options, args) = parser.parse_args(sys.argv)
print_section_header_style("Startup")
print_status_style(f"This run started {datetime.datetime.now().isoformat()}")
if options.show_tags:
dump_field_map()
sys.exit(0)
elif options.show_formats:
dump_formats()
sys.exit(0)
try:
major_mode = options.output_format
if len(args) < 2:
print_status_style(
"No input file provided, will connect to Pro Tools with PTSL..."
)
convert(major_mode=major_mode, warnings=options.warnings)
else:
convert(
input_file=args[1], major_mode=major_mode, warnings=options.warnings
)
if __name__ == "__main__":
main()
@@ -2,22 +2,26 @@
Useful functions for parsing and working with timecode.
"""
from __future__ import annotations
import math
import re
from collections import namedtuple
from fractions import Fraction
from typing import Optional, SupportsFloat
from typing import SupportsFloat
class TimecodeFormat(namedtuple("_TimecodeFormat",
"frame_duration logical_fps drop_frame")):
class TimecodeFormat(
namedtuple("_TimecodeFormat", "frame_duration logical_fps drop_frame")
):
"""
A struct reperesenting a timecode datum.
"""
def smpte_to_seconds(self, smpte: str) -> Optional[Fraction]:
def smpte_to_seconds(self, smpte: str) -> Fraction | None:
frame_count = smpte_to_frame_count(
smpte, self.logical_fps, drop_frame_hint=self.drop_frame)
smpte, self.logical_fps, drop_frame_hint=self.drop_frame
)
if frame_count is None:
return None
else:
@@ -25,12 +29,12 @@ class TimecodeFormat(namedtuple("_TimecodeFormat",
def seconds_to_smpte(self, seconds: SupportsFloat) -> str:
frame_count = int(seconds / self.frame_duration)
return frame_count_to_smpte(frame_count, self.logical_fps,
self.drop_frame)
return frame_count_to_smpte(frame_count, self.logical_fps, self.drop_frame)
def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int,
drop_frame_hint=False) -> Optional[int]:
def smpte_to_frame_count(
smpte_rep_string: str, frames_per_logical_second: int, drop_frame_hint=False
) -> int | None:
"""
Convert a string with a SMPTE timecode representation into a frame count.
@@ -44,15 +48,13 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int,
"""
assert frames_per_logical_second in [24, 25, 30, 48, 50, 60]
m = re.search(
r'(\d?\d)[:;](\d\d)[:;](\d\d)([:;])(\d\d)(\.\d+)?', smpte_rep_string)
m = re.search(r"(\d?\d)[:;](\d\d)[:;](\d\d)([:;])(\d\d)(\.\d+)?", smpte_rep_string)
if m is None:
return None
hh, mm, ss, sep, ff, frac = m.groups()
hh, mm, ss, ff, frac = int(hh), int(
mm), int(ss), int(ff), float(frac or 0.0)
hh, mm, ss, ff, frac = int(hh), int(mm), int(ss), int(ff), float(frac or 0.0)
drop_frame = drop_frame_hint
if sep == ";":
@@ -61,12 +63,16 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int,
if frames_per_logical_second not in [30, 60]:
drop_frame = False
raw_frames = hh * 3600 * frames_per_logical_second + mm * 60 * \
frames_per_logical_second + ss * frames_per_logical_second + ff
raw_frames = (
hh * 3600 * frames_per_logical_second
+ mm * 60 * frames_per_logical_second
+ ss * frames_per_logical_second
+ ff
)
frames = raw_frames
if drop_frame is True:
frames_dropped_per_inst = (frames_per_logical_second / 15)
frames_dropped_per_inst = frames_per_logical_second / 15
mins = hh * 60 + mm
inst_count = mins - math.floor(mins / 10)
dropped_frames = int(frames_dropped_per_inst) * inst_count
@@ -75,9 +81,12 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int,
return frames
def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int,
def frame_count_to_smpte(
frame_count: int,
frames_per_logical_second: int,
drop_frame: bool = False,
fractional_frame: Optional[float] = None) -> str:
fractional_frame: float | None = None,
) -> str:
assert frames_per_logical_second in [24, 25, 30, 48, 50, 60]
assert fractional_frame is None or fractional_frame < 1.0
@@ -86,7 +95,7 @@ def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int,
if drop_frame:
assert frames_per_logical_second in [30, 60]
mins, _ = divmod(nominal_frames, frames_per_logical_second * 60)
frames_dropped_per_inst = (frames_per_logical_second / 15)
frames_dropped_per_inst = frames_per_logical_second / 15
inst_count = mins - math.floor(mins / 10)
dropped_frames = frames_dropped_per_inst * inst_count
nominal_frames = nominal_frames + dropped_frames
@@ -97,15 +106,22 @@ def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int,
ss, ff = divmod(rem, frames_per_logical_second)
hh = hh % 24
hh = int(hh)
mm = int(mm)
ss = int(ss)
ff = int(ff)
if fractional_frame is not None and fractional_frame > 0:
return "%02i:%02i:%02i%s%02i%s" % (hh, mm, ss, separator, ff,
("%.3f" % fractional_frame)[1:])
fpart = "{fractional_frame:.3f}"[1:]
return f"{hh:02}:{mm:02}:{ss:02}{separator}{ff:02}{fpart}"
else:
return "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff)
return f"{hh:02}:{mm:02}:{ss:02}{separator}{ff:02}"
# "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff)
def footage_to_frame_count(footage_string) -> Optional[int]:
m = re.search(r'(\d+)\+(\d+)(\.\d+)?', footage_string)
def footage_to_frame_count(footage_string) -> int | None:
m = re.search(r"(\d+)\+(\d+)(\.\d+)?", footage_string)
if m is None:
return None
feet, frm, frac = m.groups()
@@ -118,4 +134,4 @@ def footage_to_frame_count(footage_string) -> Optional[int]:
def frame_count_to_footage(frame_count):
feet, frm = divmod(frame_count, 16)
return "%i+%02i" % (feet, frm)
return f"{feet}+{frm:02}"
@@ -2,34 +2,37 @@
This module provides the main input document parsing and transform
implementation.
"""
from __future__ import annotations
import csv
import datetime
import os
import sys
from itertools import chain
import csv
from typing import List, Optional, Iterator
from collections.abc import Iterator
from fractions import Fraction
from itertools import chain
from json import JSONEncoder
from zoneinfo import ZoneInfo
import ptsl
from .docparser.adr_entity import make_entities, ADRLine
from .reporting import print_section_header_style, print_status_style, \
print_warning
from .validations import validate_unique_field, validate_non_empty_field, \
validate_dependent_value
from ptulsconv.broadcast_timecode import TimecodeFormat
from ptulsconv.docparser import parse_document
from ptulsconv.docparser.tag_compiler import TagCompiler
from ptulsconv.broadcast_timecode import TimecodeFormat
from ptulsconv.pdf.supervisor_1pg import output_report as output_supervisor_1pg
from ptulsconv.pdf.line_count import output_report as output_line_count
from ptulsconv.pdf.talent_sides import output_report as output_talent_sides
from ptulsconv.pdf.summary_log import output_report as output_summary
from ptulsconv.pdf.continuity import output_report as output_continuity
from ptulsconv.pdf.line_count import output_report as output_line_count
from ptulsconv.pdf.summary_log import output_report as output_summary
from ptulsconv.pdf.supervisor_1pg import output_report as output_supervisor_1pg
from ptulsconv.pdf.talent_sides import output_report as output_talent_sides
from json import JSONEncoder
from .docparser.adr_entity import ADRLine, make_entities
from .reporting import print_section_header_style, print_status_style, print_warning
from .validations import (
validate_dependent_value,
validate_non_empty_field,
validate_unique_field,
)
class FractionEncoder(JSONEncoder):
@@ -37,97 +40,121 @@ class FractionEncoder(JSONEncoder):
A subclass of :class:`JSONEncoder` which encodes :class:`Fraction` objects
as a dict.
"""
force_denominator: Optional[int]
force_denominator: int | None
def default(self, o):
"""
"""
if isinstance(o, Fraction):
return dict(numerator=o.numerator, denominator=o.denominator)
return {"numerator": o.numerator, "denominator": o.denominator}
else:
return o.__dict__
def output_adr_csv(lines: List[ADRLine], time_format: TimecodeFormat):
def output_adr_csv(lines: list[ADRLine], time_format: TimecodeFormat):
"""
Writes ADR lines as CSV to the current working directory. Creates
directories for each character number and name pair, and within that
directory, creates a CSV file for each reel.
"""
reels = set([ln.reel for ln in lines])
reels: set[str | None] = {ln.reel for ln in lines}
for n, name in [(n.character_id, n.character_name) for n in lines]:
dir_name = "%s_%s" % (n, name)
dir_name = f"{n}_{name}"
os.makedirs(dir_name, exist_ok=True)
os.chdir(dir_name)
for reel in reels:
these_lines = [ln for ln in lines
if ln.character_id == n and ln.reel == reel]
these_lines = [
ln for ln in lines if ln.character_id == n and ln.reel == reel
]
if len(these_lines) == 0:
continue
outfile_name = "%s_%s_%s_%s.csv" % (these_lines[0].title, n,
these_lines[0].character_name,
reel,)
outfile_name = (
f"{these_lines[0].title}_{n}_{these_lines[0].character_name}_{reel}.csv"
)
with open(outfile_name, mode='w', newline='') as outfile:
writer = csv.writer(outfile, dialect='excel')
writer.writerow(['Title', 'Character Name', 'Cue Number',
'Reel', 'Version',
'Start', 'Finish',
'Start Seconds', 'Finish Seconds',
'Prompt',
'Reason', 'Note', 'TV'])
with open(outfile_name, mode="w", newline="") as outfile:
writer = csv.writer(outfile, dialect="excel")
writer.writerow(
[
"Title",
"Character Name",
"Cue Number",
"Reel",
"Version",
"Start",
"Finish",
"Start Seconds",
"Finish Seconds",
"Prompt",
"Reason",
"Note",
"TV",
]
)
for event in these_lines:
this_start = event.start or 0
this_finish = event.finish or 0
this_row = [event.title, event.character_name,
event.cue_number, event.reel, event.version,
this_row = [
event.title,
event.character_name,
event.cue_number,
event.reel,
event.version,
time_format.seconds_to_smpte(this_start),
time_format.seconds_to_smpte(this_finish),
float(this_start), float(this_finish),
float(this_start),
float(this_finish),
event.prompt,
event.reason, event.note, "TV"
if event.tv else ""]
event.reason,
event.note,
"TV" if event.tv else "",
]
writer.writerow(this_row)
os.chdir("..")
def generate_documents(session_tc_format, scenes, adr_lines: List[ADRLine],
title):
def generate_documents(session_tc_format, scenes, adr_lines: list[ADRLine], title):
"""
Create PDF output.
"""
print_section_header_style("Creating PDF Reports")
report_date = datetime.datetime.now()
reports_dir = "%s_%s" % (title, report_date.strftime("%Y-%m-%d_%H%M%S"))
reports_dir = f"{title}_{report_date.strftime('%Y-%m-%d_%H%M%S')}"
os.makedirs(reports_dir, exist_ok=False)
os.chdir(reports_dir)
client = next((x.client for x in adr_lines), "")
supervisor = next((x.supervisor for x in adr_lines), "")
output_continuity(scenes=scenes, tc_display_format=session_tc_format,
title=title, client=client or "",
supervisor=supervisor)
output_continuity(
scenes=scenes,
tc_display_format=session_tc_format,
title=title,
client=client or "",
supervisor=supervisor,
)
reels = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6']
reels = ["R1", "R2", "R3", "R4", "R5", "R6"]
if len(adr_lines) == 0:
print_status_style("No ADR lines were found in the input document. "
"ADR reports will not be generated.")
print_status_style(
"No ADR lines were found in the input document. "
"ADR reports will not be generated."
)
else:
create_adr_reports(adr_lines, tc_display_format=session_tc_format,
reel_list=sorted(reels))
create_adr_reports(
adr_lines, tc_display_format=session_tc_format, reel_list=sorted(reels)
)
def create_adr_reports(lines: List[ADRLine], tc_display_format: TimecodeFormat,
reel_list: List[str]):
def create_adr_reports(
lines: list[ADRLine], tc_display_format: TimecodeFormat, reel_list: list[str]
):
"""
Creates a directory heirarchy and a respective set of ADR reports,
given a list of lines.
@@ -148,8 +175,7 @@ def create_adr_reports(lines: List[ADRLine], tc_display_format: TimecodeFormat,
print_status_style("Creating Director's Logs director and reports")
os.makedirs("Director Logs", exist_ok=True)
os.chdir("Director Logs")
output_summary(lines, tc_display_format=tc_display_format,
by_character=True)
output_summary(lines, tc_display_format=tc_display_format, by_character=True)
os.chdir("..")
print_status_style("Creating CSV outputs")
@@ -178,8 +204,8 @@ def convert(major_mode, input_file=None, output=sys.stdout, warnings=True):
session_text = file.read()
else:
with ptsl.open_engine(
company_name="The ptulsconv developers",
application_name="ptulsconv") as engine:
company_name="The ptulsconv developers", application_name="ptulsconv"
) as engine:
req = engine.export_session_as_text()
req.utf8_encoding()
req.include_track_edls()
@@ -192,7 +218,7 @@ def convert(major_mode, input_file=None, output=sys.stdout, warnings=True):
session = parse_document(session_text)
session_tc_format = session.header.timecode_format
if major_mode == 'raw':
if major_mode == "raw":
output.write(FractionEncoder().encode(session))
else:
@@ -200,35 +226,35 @@ def convert(major_mode, input_file=None, output=sys.stdout, warnings=True):
compiler.session = session
compiled_events = list(compiler.compile_events())
if major_mode == 'tagged':
if major_mode == "tagged":
output.write(FractionEncoder().encode(compiled_events))
elif major_mode == 'doc':
elif major_mode == "doc":
generic_events, adr_lines = make_entities(compiled_events)
scenes = sorted([s for s in compiler.compile_all_time_spans()
if s[0] == 'Sc'],
key=lambda x: x[2])
scenes = sorted(
[s for s in compiler.compile_all_time_spans() if s[0] == "Sc"],
key=lambda x: x[2],
)
# TODO: Breakdown by titles
titles = set([x.title for x in (generic_events + adr_lines)])
titles = {x.title for x in (generic_events + adr_lines)}
if len(titles) != 1:
print_warning("Multiple titles per export is not supported, "
"found multiple titles: %s Exiting." % titles)
exit(-1)
title = list(titles)[0]
print_status_style(
"%i generic events found." % len(generic_events)
print_warning(
"Multiple titles per export is not supported, "
f"found multiple titles: {titles} Exiting."
)
print_status_style("%i ADR events found." % len(adr_lines))
sys.exit(-1)
title = next(iter(titles))
print_status_style(f"{len(generic_events)} generic events found.")
print_status_style(f"{len(adr_lines)} ADR events found.")
if warnings:
perform_adr_validations(iter(adr_lines))
generate_documents(session_tc_format, scenes, adr_lines,
title)
generate_documents(session_tc_format, scenes, adr_lines, title)
def perform_adr_validations(lines: Iterator[ADRLine]):
@@ -236,20 +262,15 @@ def perform_adr_validations(lines: Iterator[ADRLine]):
Performs validations on the input.
"""
for warning in chain(
validate_unique_field(lines,
field='cue_number',
scope='title'),
validate_non_empty_field(lines,
field='cue_number'),
validate_non_empty_field(lines,
field='character_id'),
validate_non_empty_field(lines,
field='title'),
validate_dependent_value(lines,
key_field='character_id',
dependent_field='character_name'),
validate_dependent_value(lines,
key_field='character_id',
dependent_field='actor_name')):
validate_unique_field(lines, field="cue_number", scope="title"),
validate_non_empty_field(lines, field="cue_number"),
validate_non_empty_field(lines, field="character_id"),
validate_non_empty_field(lines, field="title"),
validate_dependent_value(
lines, key_field="character_id", dependent_field="character_name"
),
validate_dependent_value(
lines, key_field="character_id", dependent_field="actor_name"
),
):
print_warning(warning.report_message())
@@ -2,4 +2,7 @@
Docparser module
"""
from .pt_doc_parser import parse_document
__all__ = [parse_document]
+140
View File
@@ -0,0 +1,140 @@
"""
This module defines classes and methods for converting :class:`Event` objects
into :class:`ADRLine` objects.
"""
from __future__ import annotations
from dataclasses import dataclass
from fractions import Fraction
from typing import ClassVar
from ptulsconv.docparser.tag_compiler import Event
from ptulsconv.docparser.tag_mapping import TagMapping
GENERIC_TAG_MAPPING = [
TagMapping(source="Title", target="title", alt=TagMapping.ContentSource.Session),
TagMapping(source="Supv", target="supervisor"),
TagMapping(source="Client", target="client"),
TagMapping(source="Sc", target="scene"),
TagMapping(source="Ver", target="version"),
TagMapping(source="Reel", target="reel"),
TagMapping(source="Note", target="note"),
TagMapping(source="Rq", target="requested_by"),
TagMapping(source="OMIT", target="omitted", formatter=(lambda x: len(x) > 0)),
]
ADR_TAG_MAPPING = [
TagMapping(source="P", target="priority"),
TagMapping(source="QN", target="cue_number"),
TagMapping(source="CN", target="character_id"),
TagMapping(
source="Char", target="character_name", alt=TagMapping.ContentSource.Track
),
TagMapping(source="Actor", target="actor_name"),
TagMapping(source="Line", target="prompt", alt=TagMapping.ContentSource.Clip),
TagMapping(source="R", target="reason"),
TagMapping(
source="Mins", target="time_budget_mins", formatter=(lambda n: float(n))
),
TagMapping(source="Spot", target="spot"),
TagMapping(source="Shot", target="shot"),
TagMapping(source="EFF", target="effort", formatter=(lambda x: len(x) > 0)),
TagMapping(source="TV", target="tv", formatter=(lambda x: len(x) > 0)),
TagMapping(source="TBW", target="tbw", formatter=(lambda x: len(x) > 0)),
TagMapping(source="ADLIB", target="adlib", formatter=(lambda x: len(x) > 0)),
TagMapping(source="OPT", target="optional", formatter=(lambda x: len(x) > 0)),
]
def make_entities(
from_events: list[Event],
) -> tuple[list[GenericEvent], list[ADRLine]]:
"""
Accepts a list of Events and converts them into either ADRLine events or
GenricEvents by calling :func:`make_entity` on each member.
:param from_events: A list of `Event` objects.
:returns: A tuple of two lists, the first containing :class:`GenericEvent`
and the second containing :class:`ADRLine`.
"""
generic_events = []
adr_lines = []
for event in from_events:
result = make_entity(event)
if type(result) is ADRLine:
adr_lines.append(result)
elif type(result) is GenericEvent:
generic_events.append(result)
return generic_events, adr_lines
def make_entity(from_event: Event) -> object | None:
"""
Accepts an event and creates either an :class:`ADRLine` or a
:class:`GenericEvent`. An event is an "ADRLine" if it has a cue number/"QN"
tag field.
:param from_event: An :class:`Event`.
"""
instance = GenericEvent
tag_map = GENERIC_TAG_MAPPING
if "QN" in from_event.tags:
instance = ADRLine
tag_map += ADR_TAG_MAPPING
new = instance()
TagMapping.apply_rules(
rules=tag_map,
tags=from_event.tags,
clip_content=from_event.clip_name,
track_content=from_event.track_name,
session_content=from_event.session_name,
to=new,
)
new.start = from_event.start
new.finish = from_event.finish
return new
@dataclass
class GenericEvent:
title: str = ""
supervisor: str | None = None
client: str | None = None
scene: str | None = None
version: str | None = None
reel: str | None = None
start: Fraction = Fraction(0, 1)
finish: Fraction = Fraction(0, 1)
omitted: bool = False
note: str | None = None
requested_by: str | None = None
tag_mapping: ClassVar = GENERIC_TAG_MAPPING
@dataclass
class ADRLine(GenericEvent):
priority: int | None = None
cue_number: str | None = None
character_id: str | None = None
character_name: str | None = None
actor_name: str | None = None
prompt: str | None = None
reason: str | None = None
time_budget_mins: float | None = None
spot: str | None = None
shot: str | None = None
effort: bool = False
tv: bool = False
tbw: bool = False
adlib: bool = False
optional: bool = False
tag_mapping: ClassVar = ADR_TAG_MAPPING
+206
View File
@@ -0,0 +1,206 @@
from __future__ import annotations
from collections.abc import Iterator
from fractions import Fraction
from ptulsconv.broadcast_timecode import TimecodeFormat
class SessionDescriptor:
header: HeaderDescriptor
files: list[FileDescriptor]
clips: list[ClipDescriptor]
plugins: list[PluginDescriptor]
tracks: list[TrackDescriptor]
markers: list[MarkerDescriptor]
def __init__(self, **kwargs):
self.header = kwargs["header"]
self.files = kwargs["files"]
self.clips = kwargs["clips"]
self.plugins = kwargs["plugins"]
self.tracks = kwargs["tracks"]
self.markers = kwargs["markers"]
def markers_timed(
self, only_ruler_markers: bool = True
) -> Iterator[tuple[MarkerDescriptor, Fraction]]:
"""
Iterate each marker in the session with its respective time reference.
"""
for marker in self.markers:
if marker.track_marker and only_ruler_markers:
continue
marker_time = Fraction(marker.time_reference, int(self.header.sample_rate))
# marker_time = self.header.convert_timecode(marker.location)
yield marker, marker_time
def tracks_clips(self) -> Iterator[tuple[TrackDescriptor, TrackClipDescriptor]]:
"""
Iterate each track clip with its respective owning clip.
"""
for track in self.tracks:
for clip in track.clips:
yield track, clip
def track_clips_timed(
self,
) -> Iterator[
tuple[TrackDescriptor, TrackClipDescriptor, Fraction, Fraction, Fraction | None]
]:
"""
Iterate each track clip with its respective owning clip and timing
information.
:returns: A Generator that yields track, clip, start time, finish time,
and timestamp
"""
for track, clip in self.tracks_clips():
start_time = self.header.convert_timecode(clip.start_timecode)
finish_time = self.header.convert_timecode(clip.finish_timecode)
timestamp_time = (
self.header.convert_timecode(clip.timestamp)
if clip.timestamp is not None
else None
)
yield track, clip, start_time, finish_time, timestamp_time
class HeaderDescriptor:
session_name: str
sample_rate: float
bit_depth: int
start_timecode: str
timecode_fps: str
timecode_drop_frame: bool
count_audio_tracks: int
count_clips: int
count_files: int
def __init__(self, **kwargs):
self.session_name = kwargs["session_name"]
self.sample_rate = kwargs["sample_rate"]
self.bit_depth = kwargs["bit_depth"]
self.start_timecode = kwargs["start_timecode"]
self.timecode_fps = kwargs["timecode_format"]
self.timecode_drop_frame = kwargs["timecode_drop_frame"]
self.count_audio_tracks = kwargs["count_audio_tracks"]
self.count_clips = kwargs["count_clips"]
self.count_files = kwargs["count_files"]
@property
def timecode_format(self):
return TimecodeFormat(
frame_duration=self.frame_duration,
logical_fps=self.logical_fps,
drop_frame=self.timecode_drop_frame,
)
def convert_timecode(self, tc_string: str) -> Fraction | None:
return self.timecode_format.smpte_to_seconds(tc_string)
@property
def start_time(self) -> Fraction | None:
"""
The start time of this session.
:return: Start time in seconds
"""
return self.convert_timecode(self.start_timecode)
@property
def logical_fps(self) -> int:
return self._get_tc_format_params[0]
@property
def frame_duration(self) -> Fraction:
return self._get_tc_format_params[1]
@property
def _get_tc_format_params(self) -> tuple[int, Fraction]:
frame_rates = {
"23.976": (24, Fraction(1001, 24_000)),
"24": (24, Fraction(1, 24)),
"25": (25, Fraction(1, 25)),
"29.97": (30, Fraction(1001, 30_000)),
"30": (30, Fraction(1, 30)),
"59.94": (60, Fraction(1001, 60_000)),
"60": (60, Fraction(1, 60)),
}
if self.timecode_fps in frame_rates:
return frame_rates[self.timecode_fps]
else:
raise ValueError(f"Unrecognized TC rate ({self.timecode_format})")
class TrackDescriptor:
index: int
name: str
comments: str
user_delay_samples: int
state: list[str]
plugins: list[str]
clips: list[TrackClipDescriptor]
def __init__(self, **kwargs):
self.index = kwargs["index"]
self.name = kwargs["name"]
self.comments = kwargs["comments"]
self.user_delay_samples = kwargs["user_delay_samples"]
self.state = kwargs["state"]
self.plugins = kwargs["plugins"]
self.clips = kwargs["clips"]
class FileDescriptor(dict):
pass
class TrackClipDescriptor:
channel: int
event: int
clip_name: str
start_timecode: str
finish_timecode: str
duration: str
timestamp: str
state: str
def __init__(self, **kwargs):
self.channel = kwargs["channel"]
self.event = kwargs["event"]
self.clip_name = kwargs["clip_name"]
self.start_timecode = kwargs["start_time"]
self.finish_timecode = kwargs["finish_time"]
self.duration = kwargs["duration"]
self.timestamp = kwargs["timestamp"]
self.state = kwargs["state"]
class ClipDescriptor(dict):
pass
class PluginDescriptor(dict):
pass
class MarkerDescriptor:
number: int
location: str
time_reference: int
units: str
name: str
comments: str
track_marker: bool
def __init__(self, **kwargs):
self.number = kwargs["number"]
self.location = kwargs["location"]
self.time_reference = kwargs["time_reference"]
self.units = kwargs["units"]
self.name = kwargs["name"]
self.comments = kwargs["comments"]
self.track_marker = kwargs["track_marker"]
@@ -1,10 +1,16 @@
from parsimonious.nodes import NodeVisitor
from parsimonious.grammar import Grammar
from parsimonious.nodes import NodeVisitor
from .doc_entity import SessionDescriptor, HeaderDescriptor, TrackDescriptor, \
FileDescriptor, TrackClipDescriptor, ClipDescriptor, PluginDescriptor, \
MarkerDescriptor
from .doc_entity import (
ClipDescriptor,
FileDescriptor,
HeaderDescriptor,
MarkerDescriptor,
PluginDescriptor,
SessionDescriptor,
TrackClipDescriptor,
TrackDescriptor,
)
protools_text_export_grammar = Grammar(
r"""
@@ -110,7 +116,8 @@ protools_text_export_grammar = Grammar(
integer_value = ~r"\d+"
float_value = ~r"\d+(\.\d+)?"
isp = ~r"[^\d\t\n]*"
""")
"""
)
def parse_document(session_text: str) -> SessionDescriptor:
@@ -124,7 +131,6 @@ def parse_document(session_text: str) -> SessionDescriptor:
class DocParserVisitor(NodeVisitor):
def __init__(self):
self.track_index = 0
@@ -137,12 +143,14 @@ class DocParserVisitor(NodeVisitor):
tracks = next(iter(visited_children[4]), None)
markers = next(iter(visited_children[5]), None)
return SessionDescriptor(header=visited_children[0],
return SessionDescriptor(
header=visited_children[0],
files=files,
clips=clips,
plugins=plugins,
tracks=tracks,
markers=markers)
markers=markers,
)
@staticmethod
def visit_header(_, visited_children):
@@ -151,7 +159,8 @@ class DocParserVisitor(NodeVisitor):
for _ in visited_children[20]:
tc_drop = True
return HeaderDescriptor(session_name=visited_children[2],
return HeaderDescriptor(
session_name=visited_children[2],
sample_rate=visited_children[6],
bit_depth=visited_children[10],
start_timecode=visited_children[15],
@@ -159,33 +168,38 @@ class DocParserVisitor(NodeVisitor):
timecode_drop_frame=tc_drop,
count_audio_tracks=visited_children[25],
count_clips=visited_children[29],
count_files=visited_children[33])
count_files=visited_children[33],
)
@staticmethod
def visit_files_section(_, visited_children):
return list(map(
lambda child: FileDescriptor(filename=child[0], path=child[2]),
visited_children[2]))
return [
FileDescriptor(filemname=child[0], path=child[2])
for child in visited_children[2]
]
@staticmethod
def visit_clips_section(_, visited_children):
channel = next(iter(visited_children[2][3]), 1)
return list(map(
lambda child: ClipDescriptor(clip_name=child[0], file=child[2],
channel=channel),
visited_children[2]))
return [
ClipDescriptor(clip_name=child[0], file=child[2], channel=channel)
for child in visited_children[2]
]
@staticmethod
def visit_plugin_listing(_, visited_children):
return list(map(lambda child:
PluginDescriptor(manufacturer=child[0],
return [
PluginDescriptor(
manufacturer=child[0],
plugin_name=child[2],
version=child[4],
format=child[6],
stems=child[8],
count_instances=child[10]),
visited_children[2]))
count_instances=child[10],
)
for child in visited_children[2]
]
# @staticmethod
def visit_track_block(self, _, visited_children):
@@ -210,7 +224,7 @@ class DocParserVisitor(NodeVisitor):
user_delay_samples=track_header[10],
state=track_header[14],
plugins=plugins,
clips=clips
clips=clips,
)
@staticmethod
@@ -227,14 +241,16 @@ class DocParserVisitor(NodeVisitor):
if isinstance(visited_children[14], list):
timestamp = visited_children[14][0][0]
return TrackClipDescriptor(channel=visited_children[0],
return TrackClipDescriptor(
channel=visited_children[0],
event=visited_children[3],
clip_name=visited_children[6],
start_time=visited_children[8],
finish_time=visited_children[10],
duration=visited_children[12],
timestamp=timestamp,
state=visited_children[15])
state=visited_children[15],
)
@staticmethod
def visit_track_state_list(_, visited_children):
@@ -252,6 +268,7 @@ class DocParserVisitor(NodeVisitor):
markers = []
for marker in visited_children[1][0][1]:
marker: MarkerDescriptor
markers.append(marker)
return markers
@@ -259,26 +276,30 @@ class DocParserVisitor(NodeVisitor):
@staticmethod
def visit_marker_record_simple(_, visited_children):
return MarkerDescriptor(number=visited_children[0],
return MarkerDescriptor(
number=visited_children[0],
location=visited_children[3],
time_reference=visited_children[5],
units=visited_children[8],
name=visited_children[10],
comments=visited_children[12],
track_marker=False)
track_marker=False,
)
@staticmethod
def visit_marker_record(_, visited_children):
track_type = visited_children[15]
is_track_marker = (track_type == "Track")
is_track_marker = track_type == "Track"
return MarkerDescriptor(number=visited_children[0],
return MarkerDescriptor(
number=visited_children[0],
location=visited_children[3],
time_reference=visited_children[5],
units=visited_children[8],
name=visited_children[10],
comments=visited_children[16],
track_marker=is_track_marker)
track_marker=is_track_marker,
)
@staticmethod
def visit_formatted_clip_name(_, visited_children):
@@ -1,11 +1,14 @@
from __future__ import annotations
from collections import namedtuple
from fractions import Fraction
from typing import Iterator, Tuple, Callable, Generator, Dict, List
import ptulsconv.docparser.doc_entity as doc_entity
from .tagged_string_parser_visitor import parse_tags, TagPreModes
from collections.abc import Generator, Iterator
from dataclasses import dataclass
from fractions import Fraction
from typing import Callable
from ptulsconv.docparser import doc_entity
from .tagged_string_parser_visitor import TagPreModes, parse_tags
@dataclass
@@ -13,7 +16,7 @@ class Event:
clip_name: str
track_name: str
session_name: str
tags: Dict[str, str]
tags: dict[str, str]
start: Fraction
finish: Fraction
@@ -24,36 +27,38 @@ class TagCompiler:
items.
"""
Intermediate = namedtuple('Intermediate',
'track_content track_tags track_comment_tags '
'clip_content clip_tags clip_tag_mode start '
'finish')
Intermediate = namedtuple(
"Intermediate",
"track_content track_tags track_comment_tags "
"clip_content clip_tags clip_tag_mode start "
"finish",
)
session: doc_entity.SessionDescriptor
def compile_all_time_spans(self) -> List[Tuple[str, str, Fraction,
Fraction]]:
def compile_all_time_spans(self) -> list[tuple[str, str, Fraction, Fraction]]:
"""
:returns: A `List` of (key: str, value: str, start: Fraction,
finish: Fraction)
"""
ret_list = list()
ret_list = []
for element in self.parse_data():
if element.clip_tag_mode == TagPreModes.TIMESPAN:
for k in element.clip_tags.keys():
ret_list.append((k, element.clip_tags[k], element.start,
element.finish))
for k in element.clip_tags:
ret_list.append(
(k, element.clip_tags[k], element.start, element.finish)
)
return ret_list
def compile_tag_list(self) -> Dict[str, List[str]]:
tags_dict = dict()
def compile_tag_list(self) -> dict[str, list[str]]:
tags_dict = {}
def update_tags_dict(other_dict: dict):
for k in other_dict.keys():
if k not in tags_dict.keys():
for k, value in other_dict.items():
if k not in tags_dict:
tags_dict[k] = set()
tags_dict[k].add(other_dict[k])
tags_dict[k].add(value)
for parsed in self.parse_data():
update_tags_dict(parsed.clip_tags)
@@ -78,15 +83,19 @@ class TagCompiler:
step3 = self.collect_time_spans(step2)
step4 = self.apply_tags(step3)
for datum in step4:
yield Event(clip_name=datum[0], track_name=datum[1],
session_name=datum[2], tags=datum[3], start=datum[4],
finish=datum[5])
yield Event(
clip_name=datum[0],
track_name=datum[1],
session_name=datum[2],
tags=datum[3],
start=datum[4],
finish=datum[5],
)
def _marker_tags(self, at):
retval = dict()
retval = {}
applicable = [(m, t) for (m, t) in
self.session.markers_timed() if t <= at]
applicable = [(m, t) for (m, t) in self.session.markers_timed() if t <= at]
for marker, _ in sorted(applicable, key=lambda x: x[1]):
retval.update(parse_tags(marker.comments or "").tag_dict)
@@ -94,21 +103,25 @@ class TagCompiler:
return retval
def filter_out_directives(self,
clips: Iterator[Intermediate]) \
-> Iterator[Intermediate]:
def filter_out_directives(
self, clips: Iterator[Intermediate]
) -> Iterator[Intermediate]:
for clip in clips:
if clip.clip_tag_mode == 'Directive':
if clip.clip_tag_mode == "Directive":
continue
else:
yield clip
@staticmethod
def _coalesce_tags(clip_tags: dict, track_tags: dict,
def _coalesce_tags(
clip_tags: dict,
track_tags: dict,
track_comment_tags: dict,
timespan_tags: dict,
marker_tags: dict, session_tags: dict):
effective_tags = dict()
marker_tags: dict,
session_tags: dict,
):
effective_tags = {}
effective_tags.update(session_tags)
effective_tags.update(marker_tags)
effective_tags.update(timespan_tags)
@@ -118,9 +131,8 @@ class TagCompiler:
return effective_tags
def parse_data(self) -> Iterator[Intermediate]:
for track, clip, start, finish, _ in self.session.track_clips_timed():
if clip.state == 'Muted':
if clip.state == "Muted":
continue
track_parsed = parse_tags(track.name)
@@ -134,15 +146,14 @@ class TagCompiler:
clip_content=clip_parsed.content,
clip_tags=clip_parsed.tag_dict,
clip_tag_mode=clip_parsed.mode,
start=start, finish=finish)
start=start,
finish=finish,
)
@staticmethod
def apply_appends(parsed: Iterator[Intermediate]) -> \
Iterator[Intermediate]:
def apply_appends(parsed: Iterator[Intermediate]) -> Iterator[Intermediate]:
def should_append(a, b):
return b.clip_tag_mode == TagPreModes.APPEND and \
b.start >= a.finish
return b.clip_tag_mode == TagPreModes.APPEND and b.start >= a.finish
def do_append(a, b):
merged_tags = dict(a.clip_tags)
@@ -151,17 +162,20 @@ class TagCompiler:
track_content=a.track_content,
track_tags=a.track_tags,
track_comment_tags=a.track_comment_tags,
clip_content=a.clip_content + ' ' + b.clip_content,
clip_tags=merged_tags, clip_tag_mode=a.clip_tag_mode,
start=a.start, finish=b.finish)
clip_content=a.clip_content + " " + b.clip_content,
clip_tags=merged_tags,
clip_tag_mode=a.clip_tag_mode,
start=a.start,
finish=b.finish,
)
yield from apply_appends(parsed, should_append, do_append)
@staticmethod
def collect_time_spans(parsed: Iterator[Intermediate]) -> \
Iterator[Tuple[Intermediate, Tuple[dict, Fraction, Fraction]]]:
time_spans = list()
def collect_time_spans(
parsed: Iterator[Intermediate],
) -> Iterator[tuple[Intermediate, tuple[dict, Fraction, Fraction]]]:
time_spans = []
for item in parsed:
if item.clip_tag_mode == TagPreModes.TIMESPAN:
@@ -171,20 +185,21 @@ class TagCompiler:
@staticmethod
def _time_span_tags(at_time: Fraction, applicable_spans) -> dict:
retval = dict()
for tags in reversed([a[0] for a in applicable_spans
if a[1] <= at_time <= a[2]]):
retval = {}
for tags in reversed(
[a[0] for a in applicable_spans if a[1] <= at_time <= a[2]]
):
retval.update(tags)
return retval
def apply_tags(self, parsed_with_time_spans) ->\
Iterator[Tuple[str, str, str, dict, Fraction, Fraction]]:
def apply_tags(
self, parsed_with_time_spans
) -> Iterator[tuple[str, str, str, dict, Fraction, Fraction]]:
session_parsed = parse_tags(self.session.header.session_name)
for event, time_spans in parsed_with_time_spans:
event: 'TagCompiler.Intermediate'
event: TagCompiler.Intermediate
marker_tags = self._marker_tags(event.start)
time_span_tags = self._time_span_tags(event.start, time_spans)
tags = self._coalesce_tags(
@@ -193,15 +208,22 @@ class TagCompiler:
track_comment_tags=event.track_comment_tags,
timespan_tags=time_span_tags,
marker_tags=marker_tags,
session_tags=session_parsed.tag_dict)
session_tags=session_parsed.tag_dict,
)
yield (event.clip_content, event.track_content,
session_parsed.content, tags, event.start, event.finish)
yield (
event.clip_content,
event.track_content,
session_parsed.content,
tags,
event.start,
event.finish,
)
def apply_appends(source: Iterator,
should_append: Callable,
do_append: Callable) -> Generator:
def apply_appends(
source: Iterator, should_append: Callable, preform_append: Callable
) -> Generator:
"""
:param source:
:param should_append: Called with two variables a and b, your
@@ -214,7 +236,7 @@ def apply_appends(source: Iterator,
this_element = next(source)
for element in source:
if should_append(this_element, element):
this_element = do_append(this_element, element)
this_element = preform_append(this_element, element)
else:
yield this_element
this_element = element
@@ -1,74 +1,81 @@
from __future__ import annotations
import sys
from enum import Enum
from typing import Optional, Callable, Any, List
from typing import Any, Callable
# from ptulsconv.docparser.adr_entity import GenericEvent
class TagMapping:
class ContentSource(Enum):
Session = 1,
Track = 2,
Clip = 3,
Session = (1,)
Track = (2,)
Clip = (3,)
source: str
alternate_source: Optional[ContentSource]
alternate_source: ContentSource | None
formatter: Callable[[str], Any]
@staticmethod
def print_rules(for_type: object, output=sys.stdout):
format_str = "%-20s | %-20s | %-25s"
hr = "%s+%s+%s" % ("-" * 21, "-" * 23, "-" * 26)
print("Tag mapping for %s" % for_type.__name__)
hr = f"{'-' * 21}+{'-' * 23}+{'-' * 26}"
print(f"Tag mapping for {for_type.__class__}")
print(hr)
print(format_str % ("Tag Source", "Target", "Type"),
file=output)
print(format_str % ("Tag Source", "Target", "Type"), file=output)
print(hr)
for rule in for_type.tag_mapping:
t = for_type.__annotations__[rule.target]
print(format_str % (rule.source, rule.target, t),
file=output)
print(format_str % (rule.source, rule.target, t), file=output)
if rule.alternate_source is TagMapping.ContentSource.Session:
print(format_str % (" - (Session Name)", rule.target, t),
file=output)
print(format_str % (" - (Session Name)", rule.target, t), file=output)
elif rule.alternate_source is TagMapping.ContentSource.Track:
print(format_str % (" - (Track Name)", rule.target, t),
file=output)
print(format_str % (" - (Track Name)", rule.target, t), file=output)
elif rule.alternate_source is TagMapping.ContentSource.Clip:
print(format_str % (" - (Clip Name)", rule.target, t),
file=output)
print(format_str % (" - (Clip Name)", rule.target, t), file=output)
@staticmethod
def apply_rules(rules: List['TagMapping'],
def apply_rules(
rules: list[TagMapping],
tags: dict,
clip_content: str,
track_content: str,
session_content: str,
to: object):
to: object,
):
done = set()
for rule in rules:
if rule.target in done:
continue
if rule.apply(tags, clip_content, track_content, session_content,
to):
if rule.apply(tags, clip_content, track_content, session_content, to):
done.update(rule.target)
def __init__(self, source: str,
def __init__(
self,
source: str,
target: str,
alt: Optional[ContentSource] = None,
formatter=None):
alt: ContentSource | None = None,
formatter=None,
):
self.source = source
self.target = target
self.alternate_source = alt
self.formatter = formatter or (lambda x: x)
def apply(self, tags: dict,
def apply(
self,
tags: dict,
clip_content: str,
track_content: str,
session_content: str, to: object) -> bool:
session_content: str,
to: object,
) -> bool:
new_value = None
if self.source in tags.keys():
if self.source in tags:
new_value = tags[self.source]
elif self.alternate_source == TagMapping.ContentSource.Session:
new_value = session_content
@@ -1,13 +1,14 @@
from parsimonious import NodeVisitor, Grammar
from typing import Dict
from enum import Enum
from typing import Dict
from parsimonious import Grammar, NodeVisitor
class TagPreModes(Enum):
NORMAL = 'Normal'
APPEND = 'Append'
TIMESPAN = 'Timespan'
DIRECTIVE = 'Directive'
NORMAL = "Normal"
APPEND = "Append"
TIMESPAN = "Timespan"
DIRECTIVE = "Directive"
tag_grammar = Grammar(
@@ -46,15 +47,14 @@ class TaggedStringResult:
class TagListVisitor(NodeVisitor):
@staticmethod
def visit_document(_, visited_children) -> TaggedStringResult:
modifier_opt, line_opt, _, tag_list_opt = visited_children
return TaggedStringResult(content=next(iter(line_opt), None),
return TaggedStringResult(
content=next(iter(line_opt), None),
tag_dict=next(iter(tag_list_opt), dict()),
mode=TagPreModes(
next(iter(modifier_opt), 'Normal'))
mode=TagPreModes(next(iter(modifier_opt), "Normal")),
)
@staticmethod
@@ -63,11 +63,11 @@ class TagListVisitor(NodeVisitor):
@staticmethod
def visit_modifier(node, _):
if node.text.startswith('@'):
if node.text.startswith("@"):
return TagPreModes.TIMESPAN
elif node.text.startswith('&'):
elif node.text.startswith("&"):
return TagPreModes.APPEND
elif node.text.startswith('!'):
elif node.text.startswith("!"):
return TagPreModes.DIRECTIVE
else:
return TagPreModes.NORMAL
@@ -1,12 +1,13 @@
"""
Methods for converting string reprentations of film footage.
"""
from fractions import Fraction
from __future__ import annotations
import re
from typing import Optional
from fractions import Fraction
def footage_to_seconds(footage: str) -> Optional[Fraction]:
def footage_to_seconds(footage: str) -> Fraction | None:
"""
Converts a string representation of a footage (35mm, 24fps)
into a :class:`Fraction`, this fraction being a some number of
@@ -15,7 +16,7 @@ def footage_to_seconds(footage: str) -> Optional[Fraction]:
:param footage: A string reprenentation of a footage of the form
resembling "90+01".
"""
m = re.match(r'(\d+)\+(\d+)(\.\d+)?', footage)
m = re.match(r"(\d+)\+(\d+)(\.\d+)?", footage)
if m is None:
return None
@@ -1,16 +1,15 @@
from __future__ import annotations
import datetime
from reportlab.pdfbase.pdfmetrics import (getAscent, getDescent)
from reportlab.lib.units import inch
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.pdfmetrics import getAscent, getDescent
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas
from reportlab.platypus.doctemplate import BaseDocTemplate, PageTemplate
from reportlab.platypus.frames import Frame
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from typing import List
# TODO: A Generic report useful for spotting
# TODO: A report useful for M&E mixer's notes
# TODO: Use a default font that doesn't need to be installed
@@ -40,12 +39,14 @@ class ReportCanvas(canvas.Canvas):
def draw_page_number(self, page_count):
self.saveState()
self.setFont('Helvetica', 10) # FIXME make this customizable
self.drawString(0.5 * inch, 0.5 * inch,
"Page %d of %d" % (self._pageNumber, page_count))
self.setFont("Helvetica", 10) # FIXME make this customizable
self.drawString(
0.5 * inch, 0.5 * inch, "Page %d of %d" % (self._pageNumber, page_count)
)
right_edge = self._pagesize[0] - 0.5 * inch
self.drawRightString(right_edge, 0.5 * inch,
self._report_date.strftime("%m/%d/%Y %H:%M"))
self.drawRightString(
right_edge, 0.5 * inch, self._report_date.strftime("%m/%d/%Y %H:%M")
)
top_line = self.beginPath()
top_line.moveTo(0.5 * inch, 0.75 * inch)
@@ -60,49 +61,60 @@ class ADRDocTemplate(BaseDocTemplate):
BaseDocTemplate.build(self, flowables, filename, canvasmaker)
def make_doc_template(page_size, filename, document_title,
def make_doc_template(
page_size,
filename,
document_title,
title: str,
supervisor: str,
document_header: str,
client: str,
document_subheader: str,
left_margin=0.5 * inch,
fonts: List[TTFont] = []) -> ADRDocTemplate:
fonts: list[TTFont] = [],
) -> ADRDocTemplate:
right_margin = top_margin = bottom_margin = 0.5 * inch
page_box = GRect(0., 0., page_size[0], page_size[1])
_, page_box = page_box.split_x(left_margin, direction='l')
_, page_box = page_box.split_x(right_margin, direction='r')
_, page_box = page_box.split_y(bottom_margin, direction='u')
_, page_box = page_box.split_y(top_margin, direction='d')
page_box: GRect = GRect(0.0, 0.0, page_size[0], page_size[1])
_, page_box = page_box.split_x(left_margin, direction="l")
_, page_box = page_box.split_x(right_margin, direction="r")
_, page_box = page_box.split_y(bottom_margin, direction="u")
_, page_box = page_box.split_y(top_margin, direction="d")
footer_box, page_box = page_box.split_y(0.25 * inch, direction='u')
header_box, page_box = page_box.split_y(0.75 * inch, direction='d')
title_box, report_box = header_box.split_x(3.5 * inch, direction='r')
footer_box, page_box = page_box.split_y(0.25 * inch, direction="u")
header_box, page_box = page_box.split_y(0.75 * inch, direction="d")
title_box, report_box = header_box.split_x(3.5 * inch, direction="r")
on_page_lambda = (lambda c, _:
draw_header_footer(c, report_box, title_box,
footer_box, title=title,
def on_page_lambda(c, _):
draw_header_footer(
c,
report_box,
title_box,
footer_box,
title=title,
supervisor=supervisor,
document_subheader=document_subheader,
client=client,
doc_title=document_header))
doc_title=document_header,
)
frames = [Frame(page_box.min_x, page_box.min_y,
page_box.width, page_box.height)]
page_template = PageTemplate(id="Main",
frames=frames,
onPage=on_page_lambda)
frames = [Frame(page_box.min_x, page_box.min_y, page_box.width, page_box.height)]
page_template = PageTemplate(id="Main", frames=frames, onPage=on_page_lambda)
for font in fonts:
pdfmetrics.registerFont(font)
doc = ADRDocTemplate(filename,
doc = ADRDocTemplate(
filename,
title=document_title,
author=supervisor,
pagesize=page_size,
leftMargin=left_margin, rightMargin=right_margin,
topMargin=top_margin, bottomMargin=bottom_margin)
leftMargin=left_margin,
rightMargin=right_margin,
topMargin=top_margin,
bottomMargin=bottom_margin,
)
doc.addPageTemplates([page_template])
@@ -115,27 +127,43 @@ def time_format(mins, zero_str="-"):
"""
if mins is None:
return zero_str
if mins == 0. and zero_str is not None:
if mins == 0.0 and zero_str is not None:
return zero_str
elif mins < 60.:
return "%im" % round(mins)
elif mins < 60.0:
return f"{round(mins)}m"
else:
m = round(mins)
hh, mm = divmod(m, 60)
return "%i:%02i" % (hh, mm)
return f"{hh}:{mm:02}"
def draw_header_footer(a_canvas: ReportCanvas, left_box, right_box,
footer_box, title: str, supervisor: str,
document_subheader: str, client: str, doc_title="",
font_name='Helvetica'):
def draw_header_footer(
a_canvas: ReportCanvas,
left_box,
right_box,
footer_box,
title: str,
supervisor: str,
document_subheader: str,
client: str,
doc_title="",
font_name="Helvetica",
):
(_supervisor_box, client_box,), title_box = \
right_box.divide_y([16., 16., ])
title_box.draw_text_cell(a_canvas, title, font_name, 18,
inset_y=2., inset_x=5.)
client_box.draw_text_cell(a_canvas, client, font_name, 11,
inset_y=2., inset_x=5.)
(
(
_supervisor_box,
client_box,
),
title_box,
) = right_box.divide_y(
[
16.0,
16.0,
]
)
title_box.draw_text_cell(a_canvas, title, font_name, 18, inset_y=2.0, inset_x=5.0)
client_box.draw_text_cell(a_canvas, client, font_name, 11, inset_y=2.0, inset_x=5.0)
a_canvas.saveState()
a_canvas.setLineWidth(0.5)
@@ -150,20 +178,26 @@ def draw_header_footer(a_canvas: ReportCanvas, left_box, right_box,
a_canvas.drawPath(tline2)
a_canvas.restoreState()
(doc_title_cell, spotting_version_cell,), _ = \
left_box.divide_y([18., 14], direction='d')
(
(
doc_title_cell,
spotting_version_cell,
),
_,
) = left_box.divide_y([18.0, 14], direction="d")
doc_title_cell.draw_text_cell(a_canvas, doc_title, font_name, 14.,
inset_y=2.)
doc_title_cell.draw_text_cell(a_canvas, doc_title, font_name, 14.0, inset_y=2.0)
if document_subheader is not None:
spotting_version_cell.draw_text_cell(a_canvas, document_subheader,
font_name, 12., inset_y=2.)
spotting_version_cell.draw_text_cell(
a_canvas, document_subheader, font_name, 12.0, inset_y=2.0
)
if supervisor is not None:
a_canvas.setFont(font_name, 11.)
a_canvas.drawCentredString(footer_box.min_x + footer_box.width / 2.,
footer_box.min_y, supervisor)
a_canvas.setFont(font_name, 11.0)
a_canvas.drawCentredString(
footer_box.min_x + footer_box.width / 2.0, footer_box.min_y, supervisor
)
class GRect:
@@ -200,57 +234,61 @@ class GRect:
return self.y + self.height / 2
def normalize(self):
if self.width < 0.:
if self.width < 0.0:
self.width = abs(self.width)
self.x = self.x - self.width
if self.height < 0.:
if self.height < 0.0:
self.height = abs(self.height)
self.y = self.y - self.height
def split_x(self, at, direction='l'):
def split_x(self, at, direction="l"):
if at >= self.width:
return None, self
elif at <= 0:
return self, None
else:
if direction == 'l':
return (GRect(self.min_x, self.min_y, at, self.height),
GRect(self.min_x + at, self.y,
self.width - at, self.height))
if direction == "l":
return (
GRect(self.min_x, self.min_y, at, self.height),
GRect(self.min_x + at, self.y, self.width - at, self.height),
)
else:
return (GRect(self.max_x - at, self.y, at, self.height),
GRect(self.min_x, self.y,
self.width - at, self.height))
return (
GRect(self.max_x - at, self.y, at, self.height),
GRect(self.min_x, self.y, self.width - at, self.height),
)
def split_y(self, at, direction='u'):
def split_y(self, at, direction="u"):
if at >= self.height:
return None, self
elif at <= 0:
return self, None
else:
if direction == 'u':
return (GRect(self.x, self.y, self.width, at),
GRect(self.x, self.y + at,
self.width, self.height - at))
if direction == "u":
return (
GRect(self.x, self.y, self.width, at),
GRect(self.x, self.y + at, self.width, self.height - at),
)
else:
return (GRect(self.x, self.max_y - at, self.width, at),
GRect(self.x, self.y,
self.width, self.height - at))
return (
GRect(self.x, self.max_y - at, self.width, at),
GRect(self.x, self.y, self.width, self.height - at),
)
def inset_xy(self, dx, dy):
return GRect(self.x + dx, self.y + dy,
self.width - dx * 2, self.height - dy * 2)
return GRect(
self.x + dx, self.y + dy, self.width - dx * 2, self.height - dy * 2
)
def inset(self, d):
return self.inset_xy(d, d)
def __repr__(self):
return "<GRect x=%f y=%f width=%f height=%f>" % \
(self.x, self.y, self.width, self.height)
return f"<GRect x={self.x} y={self.y} width={self.width} height={self.height}>"
def divide_x(self, x_list, direction='l'):
ret_list = list()
def divide_x(self, x_list, direction="l"):
ret_list = []
rem = self
for item in x_list:
@@ -259,8 +297,8 @@ class GRect:
return ret_list, rem
def divide_y(self, y_list, direction='u'):
ret_list = list()
def divide_y(self, y_list, direction="u"):
ret_list = []
rem = self
for item in y_list:
@@ -279,18 +317,14 @@ class GRect:
def draw_border(self, a_canvas, edge):
def draw_border_impl(en):
if en == 'min_x':
coordinates = ((self.min_x, self.min_y),
(self.min_x, self.max_y))
elif en == 'max_x':
coordinates = ((self.max_x, self.min_y),
(self.max_x, self.max_y))
elif en == 'min_y':
coordinates = ((self.min_x, self.min_y),
(self.max_x, self.min_y))
elif en == 'max_y':
coordinates = ((self.min_x, self.max_y),
(self.max_x, self.max_y))
if en == "min_x":
coordinates = ((self.min_x, self.min_y), (self.min_x, self.max_y))
elif en == "max_x":
coordinates = ((self.max_x, self.min_y), (self.max_x, self.max_y))
elif en == "min_y":
coordinates = ((self.min_x, self.min_y), (self.max_x, self.min_y))
elif en == "max_y":
coordinates = ((self.min_x, self.max_y), (self.max_x, self.max_y))
else:
return
@@ -305,9 +339,18 @@ class GRect:
for e in edge:
draw_border_impl(e)
def draw_text_cell(self, a_canvas, text, font_name, font_size,
vertical_align='t', force_baseline=None, inset_x=0.,
inset_y=0., draw_baseline=False):
def draw_text_cell(
self,
a_canvas,
text,
font_name,
font_size,
vertical_align="t",
force_baseline=None,
inset_x=0.0,
inset_y=0.0,
draw_baseline=False,
):
if text is None:
return
@@ -315,9 +358,9 @@ class GRect:
inset_rect = self.inset_xy(inset_x, inset_y)
if vertical_align == 'm':
y = inset_rect.center_y - getAscent(font_name, font_size) / 2.
elif vertical_align == 't':
if vertical_align == "m":
y = inset_rect.center_y - getAscent(font_name, font_size) / 2.0
elif vertical_align == "t":
y = inset_rect.max_y - getAscent(font_name, font_size)
else:
y = inset_rect.min_y - getDescent(font_name, font_size)
@@ -339,14 +382,15 @@ class GRect:
a_canvas.setDash([3.0, 1.0, 2.0, 1.0])
a_canvas.setLineWidth(0.5)
bl = a_canvas.beginPath()
bl.moveTo(inset_rect.min_x, y - 1.)
bl.lineTo(inset_rect.max_x, y - 1.)
bl.moveTo(inset_rect.min_x, y - 1.0)
bl.lineTo(inset_rect.max_x, y - 1.0)
a_canvas.drawPath(bl)
a_canvas.restoreState()
def draw_flowable(self, a_canvas, flowable, inset_x=0.,
inset_y=0., draw_baselines=False):
def draw_flowable(
self, a_canvas, flowable, inset_x=0.0, inset_y=0.0, draw_baselines=False
):
a_canvas.saveState()
inset_rect = self.inset_xy(inset_x, inset_y)
@@ -364,7 +408,7 @@ class GRect:
a_canvas.setLineWidth(0.5)
leading = flowable.style.leading
y = inset_rect.max_y - flowable.style.fontSize - 1.
y = inset_rect.max_y - flowable.style.fontSize - 1.0
while y > inset_rect.min_x:
bl = a_canvas.beginPath()
bl.moveTo(inset_rect.min_x, y)
+63
View File
@@ -0,0 +1,63 @@
from fractions import Fraction
from reportlab.lib.pagesizes import letter, portrait
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import Paragraph, Table
from ptulsconv.broadcast_timecode import TimecodeFormat
from ptulsconv.pdf import make_doc_template
def table_for_scene(scene, tc_format, font_name="Helvetica"):
scene_style = getSampleStyleSheet()["Normal"]
scene_style.fontName = font_name
scene_style.leftIndent = 0.0
scene_style.leftPadding = 0.0
scene_style.spaceAfter = 18.0
tc_data = f"<em>{tc_format.seconds_to_smpte(scene[2])}</em><br />{tc_format.seconds_to_smpte(scene[3])}"
row = [
Paragraph(tc_data, scene_style),
Paragraph(scene[1], scene_style),
]
style = [
("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (0, 0), 0.0),
("BOTTOMPADDING", (0, 0), (-1, -1), 12.0),
("FONTNAME", (0, 0), (-1, -1), font_name),
]
return Table(data=[row], style=style, colWidths=[1.0 * inch, 6.5 * inch])
def output_report(
scenes: list[tuple[str, str, Fraction, Fraction]],
tc_display_format: TimecodeFormat,
title: str,
client: str,
supervisor,
paper_size=letter,
):
filename = f"{title} Continuity.pdf"
document_header = "Continuity"
doc = make_doc_template(
page_size=portrait(paper_size),
filename=filename,
document_title="Continuity",
title=title,
client=client,
document_subheader="",
supervisor=supervisor,
document_header=document_header,
left_margin=0.5 * inch,
)
story = []
# story.append(Spacer(height=0.5 * inch, width=1.))
for scene in scenes:
story.append(table_for_scene(scene, tc_display_format))
doc.build(story)
+371
View File
@@ -0,0 +1,371 @@
from typing import List, Optional
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter, portrait
from reportlab.lib.styles import getSampleStyleSheet
# from reportlab.pdfbase import pdfmetrics
# from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.units import inch
from reportlab.platypus import Paragraph, Spacer, Table
from ..docparser.adr_entity import ADRLine
from .__init__ import make_doc_template, time_format
def build_columns(
lines: List[ADRLine],
reel_list: Optional[List[str]],
show_priorities=False,
include_omitted=False,
):
columns = list()
reel_numbers = reel_list or sorted(
set([x.reel for x in lines if x.reel is not None])
)
num_column_width = 15.0 / 32.0 * inch
columns.append(
{
"heading": "#",
"value_getter": lambda recs: recs[0].character_id,
"value_getter2": lambda recs: "",
"style_getter": lambda col_index: [],
"width": 0.375 * inch,
"summarize": False,
}
)
columns.append(
{
"heading": "Role",
"value_getter": lambda recs: recs[0].character_name,
"value_getter2": lambda recs: recs[0].actor_name or "",
"style_getter": lambda col_index: [
("LINEAFTER", (col_index, 0), (col_index, -1), 1.0, colors.black)
],
"width": 1.75 * inch,
"summarize": False,
}
)
columns.append(
{
"heading": "TV",
"value_getter": lambda recs: len([r for r in recs if r.tv]),
"value_getter2": (
lambda recs: time_format(
sum([r.time_budget_mins or 0.0 for r in recs if r.tv])
)
),
"style_getter": (
lambda col_index: [
("ALIGN", (col_index, 0), (col_index, -1), "CENTER"),
("LINEBEFORE", (col_index, 0), (col_index, -1), 1.0, colors.black),
("LINEAFTER", (col_index, 0), (col_index, -1), 0.5, colors.gray),
]
),
"width": num_column_width,
}
)
columns.append(
{
"heading": "Opt",
"value_getter": lambda recs: len([r for r in recs if r.optional]),
"value_getter2": (
lambda recs: time_format(
sum([r.time_budget_mins or 0.0 for r in recs if r.optional])
)
),
"style_getter": (
lambda col_index: [
("ALIGN", (col_index, 0), (col_index, -1), "CENTER"),
("LINEAFTER", (col_index, 0), (col_index, -1), 0.5, colors.gray),
]
),
"width": num_column_width,
}
)
columns.append(
{
"heading": "Eff",
"value_getter": lambda recs: len([r for r in recs if r.effort]),
"value_getter2": (
lambda recs: time_format(
sum([r.time_budget_mins or 0.0 for r in recs if r.effort])
)
),
"style_getter": (
lambda col_index: [("ALIGN", (col_index, 0), (col_index, -1), "CENTER")]
),
"width": num_column_width,
}
)
columns.append(
{
"heading": "",
"value_getter": lambda _: "",
"value_getter2": lambda _: "",
"style_getter": lambda col_index: [
("LINEBEFORE", (col_index, 0), (col_index, -1), 1.0, colors.black),
("LINEAFTER", (col_index, 0), (col_index, -1), 1.0, colors.black),
],
"width": 2.0,
}
)
if len(reel_numbers) > 0:
for n in reel_numbers:
columns.append(
{
"heading": n,
"value_getter": (
lambda recs, n1=n: len([r for r in recs if r.reel == n1])
),
"value_getter2": (
lambda recs, n1=n: time_format(
sum(
[
r.time_budget_mins or 0.0
for r in recs
if r.reel == n1
]
)
)
),
"style_getter": (
lambda col_index: [
("ALIGN", (col_index, 0), (col_index, -1), "CENTER"),
(
"LINEAFTER",
(col_index, 0),
(col_index, -1),
0.5,
colors.gray,
),
]
),
"width": num_column_width,
}
)
if show_priorities:
for n in range(
1,
6,
):
columns.append(
{
"heading": "P%i" % n,
"value_getter": lambda recs: len(
[r for r in recs if r.priority == n]
),
"value_getter2": (
lambda recs: time_format(
sum(
[
r.time_budget_mins or 0.0
for r in recs
if r.priority == n
]
)
)
),
"style_getter": lambda col_index: [],
"width": num_column_width,
}
)
columns.append(
{
"heading": ">P5",
"value_getter": lambda recs: len(
[r for r in recs if (r.priority or 5) > 5]
),
"value_getter2": (
lambda recs: time_format(
sum(
[
r.time_budget_mins or 0.0
for r in recs
if (r.priority or 5) > 5
]
)
)
),
"style_getter": lambda col_index: [],
"width": num_column_width,
}
)
if include_omitted:
columns.append(
{
"heading": "Omit",
"value_getter": lambda recs: len([r for r in recs if r.omitted]),
"value_getter2": (
lambda recs: time_format(
sum([r.time_budget_mins or 0.0 for r in recs if r.omitted])
)
),
"style_getter": (
lambda col_index: [
("ALIGN", (col_index, 0), (col_index, -1), "CENTER")
]
),
"width": num_column_width,
}
)
columns.append(
{
"heading": "Total",
"value_getter": lambda recs: len([r for r in recs if not r.omitted]),
"value_getter2": (
lambda recs: time_format(
sum([r.time_budget_mins or 0.0 for r in recs if not r.omitted])
)
),
"style_getter": (
lambda col_index: [
("LINEBEFORE", (col_index, 0), (col_index, -1), 1.0, colors.black),
("ALIGN", (col_index, 0), (col_index, -1), "CENTER"),
]
),
"width": 0.5 * inch,
}
)
return columns
def populate_columns(lines: List[ADRLine], columns, include_omitted, _page_size):
data = list()
styles = list()
columns_widths = list()
sorted_character_numbers: List[str] = sorted(
set([x.character_id for x in lines]), key=lambda x: str(x)
)
# construct column styles
for i, c in enumerate(columns):
styles.extend(c["style_getter"](i))
columns_widths.append(c["width"])
data.append(list(map(lambda x: x["heading"], columns)))
if not include_omitted:
lines = [x for x in lines if not x.omitted]
for n in sorted_character_numbers:
char_records = [x for x in lines if x.character_id == n]
if len(char_records) > 0:
row_data = list()
row_data2 = list()
for col in columns:
row1_index = len(data)
row2_index = row1_index + 1
row_data.append(col["value_getter"](list(char_records)))
row_data2.append(col["value_getter2"](list(char_records)))
styles.extend(
[
("TEXTCOLOR", (0, row2_index), (-1, row2_index), colors.red),
(
"LINEBELOW",
(0, row2_index),
(-1, row2_index),
0.5,
colors.black,
),
]
)
data.append(row_data)
data.append(row_data2)
summary_row1 = list()
summary_row2 = list()
row1_index = len(data)
for col in columns:
if col.get("summarize", True):
summary_row1.append(col["value_getter"](lines))
summary_row2.append(col["value_getter2"](lines))
else:
summary_row1.append("")
summary_row2.append("")
styles.append(("LINEABOVE", (0, row1_index), (-1, row1_index), 2.0, colors.black))
data.append(summary_row1)
data.append(summary_row2)
return data, styles, columns_widths
# def build_header(column_widths):
# pass
def output_report(
lines: List[ADRLine],
reel_list: List[str],
include_omitted=False,
page_size=portrait(letter),
font_name="Helvetica",
):
columns = build_columns(lines, include_omitted=include_omitted, reel_list=reel_list)
data, style, columns_widths = populate_columns(
lines, columns, include_omitted, page_size
)
style.append(("FONTNAME", (0, 0), (-1, -1), font_name))
style.append(("FONTSIZE", (0, 0), (-1, -1), 9.0))
style.append(("LINEBELOW", (0, 0), (-1, 0), 1.0, colors.black))
# style.append(('LINEBELOW', (0, 1), (-1, -1), 0.25, colors.gray))
# pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
title = "%s Line Count" % lines[0].title
filename = title + ".pdf"
doc = make_doc_template(
page_size=page_size,
filename=filename,
document_title=title,
title=lines[0].title,
document_subheader=lines[0].spot,
client=lines[0].client,
supervisor=lines[0].supervisor,
document_header="Line Count",
)
# header_data, header_style, header_widths = build_header(columns_widths)
# header_table = Table(data=header_data, style=header_style,
# colWidths=header_widths)
table = Table(data=data, style=style, colWidths=columns_widths)
story = [Spacer(height=0.5 * inch, width=1.0), table]
style = getSampleStyleSheet()["Normal"]
style.fontName = font_name
style.fontSize = 12.0
style.spaceBefore = 16.0
style.spaceAfter = 16.0
omitted_count = len([x for x in lines if x.omitted])
if not include_omitted and omitted_count > 0:
story.append(
Paragraph("* %i Omitted lines are excluded." % omitted_count, style)
)
doc.build(story)
@@ -1,5 +1,6 @@
# TODO: Complete Recordist Log
def output_report(records):
# order by start
+195
View File
@@ -0,0 +1,195 @@
from typing import List
from reportlab.lib.pagesizes import letter, portrait
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table
from ptulsconv.broadcast_timecode import TimecodeFormat
from ptulsconv.docparser.adr_entity import ADRLine
from .__init__ import make_doc_template, time_format
def build_aux_data_field(line: ADRLine):
entries = list()
if line.reason is not None:
entries.append("Reason: " + line.reason)
if line.note is not None:
entries.append("Note: " + line.note)
if line.requested_by is not None:
entries.append("Requested by: " + line.requested_by)
if line.shot is not None:
entries.append("Shot: " + line.shot)
fg_color = "white"
tag_field = ""
if line.effort:
bg_color = "red"
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " % (
bg_color,
fg_color,
"EFF",
)
elif line.tv:
bg_color = "blue"
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " % (
bg_color,
fg_color,
"TV",
)
elif line.adlib:
bg_color = "purple"
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " % (
bg_color,
fg_color,
"ADLIB",
)
elif line.optional:
bg_color = "green"
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font>" % (
bg_color,
fg_color,
"OPTIONAL",
)
entries.append(tag_field)
return "<br />".join(entries)
def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat, font_name="Helvetica"):
story = list()
this_scene = None
scene_style = getSampleStyleSheet()["Normal"]
scene_style.fontName = font_name
scene_style.leftIndent = 0.0
scene_style.leftPadding = 0.0
scene_style.spaceAfter = 18.0
line_style = getSampleStyleSheet()["Normal"]
line_style.fontName = font_name
for line in lines:
table_style = [
("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (0, 0), 0.0),
("BOTTOMPADDING", (0, 0), (-1, -1), 24.0),
]
cue_number_field = "%s<br /><font fontSize=7>%s</font>" % (
line.cue_number,
line.character_name,
)
time_data = time_format(line.time_budget_mins)
if line.priority is not None:
time_data = time_data + "<br />" + "P: " + line.priority
aux_data_field = build_aux_data_field(line)
tc_data = build_tc_data(line, tc_rate)
line_table_data = [
[
Paragraph(cue_number_field, line_style),
Paragraph(tc_data, line_style),
Paragraph(line.prompt, line_style),
Paragraph(time_data, line_style),
Paragraph(aux_data_field, line_style),
]
]
line_table = Table(
data=line_table_data,
colWidths=[inch * 0.75, inch, inch * 3.0, 0.5 * inch, inch * 2.0],
style=table_style,
)
if (line.scene or "[No Scene]") != this_scene:
this_scene = line.scene or "[No Scene]"
story.append(
KeepTogether(
[
Spacer(1.0, 0.25 * inch),
Paragraph("<u>" + this_scene + "</u>", scene_style),
line_table,
]
)
)
else:
line_table.setStyle(table_style)
story.append(KeepTogether([line_table]))
return story
def build_tc_data(line: ADRLine, tc_format: TimecodeFormat):
tc_data = (
tc_format.seconds_to_smpte(line.start)
+ "<br />"
+ tc_format.seconds_to_smpte(line.finish)
)
third_line = []
if line.reel is not None:
if line.reel[0:1] == "R":
third_line.append("%s" % line.reel)
else:
third_line.append("Reel %s" % line.reel)
if line.version is not None:
third_line.append("(%s)" % line.version)
if len(third_line) > 0:
tc_data = tc_data + "<br/>" + " ".join(third_line)
return tc_data
def generate_report(
page_size,
lines: List[ADRLine],
tc_rate: TimecodeFormat,
character_number=None,
include_omitted=True,
):
if character_number is not None:
lines = [r for r in lines if r.character_id == character_number]
title = "%s ADR Report (%s)" % (lines[0].title, lines[0].character_name)
document_header = "%s ADR Report" % lines[0].character_name
else:
title = "%s ADR Report" % lines[0].title
document_header = "ADR Report"
if not include_omitted:
lines = [line for line in lines if not line.omitted]
lines = sorted(lines, key=lambda line: line.start)
filename = title + ".pdf"
doc = make_doc_template(
page_size=page_size,
filename=filename,
document_title=title,
document_header=document_header,
title=lines[0].title,
supervisor=lines[0].supervisor,
client=lines[0].client,
document_subheader=lines[0].spot,
left_margin=0.75 * inch,
)
story = build_story(lines, tc_rate)
doc.build(story)
def output_report(
lines: List[ADRLine],
tc_display_format: TimecodeFormat,
page_size=portrait(letter),
by_character=False,
):
if by_character:
character_numbers = set(r.character_id for r in lines)
for n in character_numbers:
generate_report(page_size, lines, tc_display_format, n)
else:
generate_report(page_size, lines, tc_display_format)
+349
View File
@@ -0,0 +1,349 @@
import datetime
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet
# from reportlab.pdfbase import pdfmetrics
# from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.units import inch
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from ptulsconv.broadcast_timecode import TimecodeFormat
from ptulsconv.docparser.adr_entity import ADRLine
from .__init__ import GRect
font_name = "Helvetica"
def draw_header_block(canvas, rect, record: ADRLine):
rect.draw_text_cell(canvas, record.cue_number, "Helvetica", 44, vertical_align="m")
def draw_character_row(canvas, rect, record: ADRLine):
label_frame, value_frame = rect.split_x(1.25 * inch)
label_frame.draw_text_cell(canvas, "CHARACTER", font_name, 10, force_baseline=9.0)
line = "%s / %s" % (record.character_id, record.character_name)
if record.actor_name is not None:
line = line + " / " + record.actor_name
value_frame.draw_text_cell(canvas, line, font_name, 12, force_baseline=9.0)
rect.draw_border(canvas, ["min_y", "max_y"])
def draw_cue_number_block(canvas, rect, record: ADRLine):
(
(
label_frame,
number_frame,
),
aux_frame,
) = rect.divide_y([0.20 * inch, 0.375 * inch], direction="d")
label_frame.draw_text_cell(
canvas, "CUE NUMBER", font_name, 10, inset_y=5.0, vertical_align="t"
)
number_frame.draw_text_cell(
canvas,
record.cue_number,
font_name,
14,
inset_x=10.0,
inset_y=2.0,
draw_baseline=True,
)
tags = {
"tv": "TV",
"optional": "OPT",
"adlib": "ADLIB",
"effort": "EFF",
"tbw": "TBW",
"omitted": "OMIT",
}
tag_field = ""
for key in tags:
if getattr(record, key):
tag_field = tag_field + tags[key] + " "
aux_frame.draw_text_cell(
canvas, tag_field, font_name, 10, inset_x=10.0, inset_y=2.0, vertical_align="t"
)
rect.draw_border(canvas, "max_x")
def draw_timecode_block(
canvas, rect, record: ADRLine, tc_display_format: TimecodeFormat
):
(in_label_frame, in_frame, out_label_frame, out_frame), _ = rect.divide_y(
[0.20 * inch, 0.25 * inch, 0.20 * inch, 0.25 * inch], direction="d"
)
in_label_frame.draw_text_cell(
canvas, "IN", font_name, 10, vertical_align="t", inset_y=5.0, inset_x=5.0
)
in_frame.draw_text_cell(
canvas,
tc_display_format.seconds_to_smpte(record.start),
font_name,
14,
inset_x=10.0,
inset_y=2.0,
draw_baseline=True,
)
out_label_frame.draw_text_cell(
canvas, "OUT", font_name, 10, vertical_align="t", inset_y=5.0, inset_x=5.0
)
out_frame.draw_text_cell(
canvas,
tc_display_format.seconds_to_smpte(record.finish),
font_name,
14,
inset_x=10.0,
inset_y=2.0,
draw_baseline=True,
)
rect.draw_border(canvas, "max_x")
def draw_reason_block(canvas, rect, record: ADRLine):
reason_cell, notes_cell = rect.split_y(24.0, direction="d")
reason_label, reason_value = reason_cell.split_x(0.75 * inch)
notes_label, notes_value = notes_cell.split_x(0.75 * inch)
reason_label.draw_text_cell(
canvas, "Reason:", font_name, 12, inset_x=5.0, inset_y=5.0, vertical_align="b"
)
reason_value.draw_text_cell(
canvas,
record.reason or "",
font_name,
12,
inset_x=5.0,
inset_y=5.0,
draw_baseline=True,
vertical_align="b",
)
notes_label.draw_text_cell(
canvas, "Note:", font_name, 12, inset_x=5.0, inset_y=5.0, vertical_align="t"
)
style = getSampleStyleSheet()["BodyText"]
style.fontName = font_name
style.fontSize = 12
style.leading = 14
p = Paragraph(record.note or "", style)
notes_value.draw_flowable(canvas, p, draw_baselines=True, inset_x=5.0, inset_y=5.0)
def draw_prompt(canvas, rect, prompt=""):
label, block = rect.split_y(0.20 * inch, direction="d")
label.draw_text_cell(
canvas, "PROMPT", font_name, 10, vertical_align="t", inset_y=5.0, inset_x=0.0
)
style = getSampleStyleSheet()["BodyText"]
style.fontName = font_name
style.fontSize = 14
style.leading = 24
style.leftIndent = 1.5 * inch
style.rightIndent = 1.5 * inch
p = Paragraph(prompt, style)
block.draw_flowable(canvas, p, draw_baselines=True)
rect.draw_border(canvas, "max_y")
def draw_notes(canvas, rect, note=""):
label, block = rect.split_y(0.20 * inch, direction="d")
label.draw_text_cell(
canvas, "NOTES", font_name, 10, vertical_align="t", inset_y=5.0, inset_x=0.0
)
style = getSampleStyleSheet()["BodyText"]
style.fontName = font_name
style.fontSize = 14
style.leading = 24
prompt = Paragraph(note, style)
block.draw_flowable(canvas, prompt, draw_baselines=True)
rect.draw_border(canvas, ["max_y", "min_y"])
def draw_take_grid(canvas, rect):
canvas.saveState()
cp = canvas.beginPath()
cp.rect(rect.min_x, rect.min_y, rect.width, rect.height)
canvas.clipPath(cp, stroke=0, fill=0)
canvas.setDash([3.0, 2.0])
for xi in range(1, 10):
x = xi * (rect.width / 10)
if xi % 5 == 0:
canvas.setDash(1, 0)
else:
canvas.setDash([2, 5])
ln = canvas.beginPath()
ln.moveTo(rect.min_x + x, rect.min_y)
ln.lineTo(rect.min_x + x, rect.max_y)
canvas.drawPath(ln)
for yi in range(1, 10):
y = yi * (rect.height / 6)
if yi % 2 == 0:
canvas.setDash(1, 0)
else:
canvas.setDash([2, 5])
ln = canvas.beginPath()
ln.moveTo(rect.min_x, rect.min_y + y)
ln.lineTo(rect.max_x, rect.min_y + y)
canvas.drawPath(ln)
rect.draw_border(canvas, "max_x")
canvas.restoreState()
def draw_aux_block(canvas, rect, recording_time_sec_this_line, recording_time_sec):
rect.draw_border(canvas, "min_x")
content_rect = rect.inset_xy(10.0, 10.0)
lines, last_line = content_rect.divide_y(
[12.0, 12.0, 24.0, 24.0, 24.0, 24.0], direction="d"
)
lines[0].draw_text_cell(
canvas,
"Time for this line: %.1f mins" % (recording_time_sec_this_line / 60.0),
font_name,
9.0,
)
lines[1].draw_text_cell(
canvas,
"Running time: %03.1f mins" % (recording_time_sec / 60.0),
font_name,
9.0,
)
lines[2].draw_text_cell(
canvas, "Actual Start: ______________", font_name, 9.0, vertical_align="b"
)
lines[3].draw_text_cell(
canvas, "Record Date: ______________", font_name, 9.0, vertical_align="b"
)
lines[4].draw_text_cell(
canvas, "Engineer: ______________", font_name, 9.0, vertical_align="b"
)
lines[5].draw_text_cell(
canvas, "Location: ______________", font_name, 9.0, vertical_align="b"
)
def draw_footer(canvas, rect, record: ADRLine, report_date, line_no, total_lines):
rect.draw_border(canvas, "max_y")
report_date_s = [report_date.strftime("%c")]
spotting_name = [record.spot] if record.spot is not None else []
pages_s = ["Line %i of %i" % (line_no, total_lines)]
footer_s = " - ".join(report_date_s + spotting_name + pages_s)
rect.draw_text_cell(
canvas, footer_s, font_name=font_name, font_size=10.0, inset_y=2.0
)
def create_report_for_character(
records, report_date, tc_display_format: TimecodeFormat
):
outfile = "%s_%s_%s_Log.pdf" % (
records[0].title,
records[0].character_id,
records[0].character_name,
)
assert outfile is not None
assert outfile[-4:] == ".pdf", "Output file must have 'pdf' extension!"
# pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
page: GRect = GRect(0, 0, letter[0], letter[1])
page = page.inset(inch * 0.5)
(header_row, char_row, data_row, prompt_row, notes_row, takes_row), footer = (
page.divide_y(
[0.875 * inch, 0.375 * inch, inch, 3.0 * inch, 1.5 * inch, 3 * inch],
direction="d",
)
)
cue_header_block, title_header_block = header_row.split_x(4.0 * inch)
(cue_number_block, timecode_block), reason_block = data_row.divide_x(
[1.5 * inch, 1.5 * inch]
)
(take_grid_block), aux_block = takes_row.split_x(5.25 * inch)
c = Canvas(
outfile,
pagesize=letter,
)
c.setTitle(
"%s %s (%s) Supervisor's Log"
% (records[0].title, records[0].character_name, records[0].character_id)
)
c.setAuthor(records[0].supervisor)
recording_time_sec = 0.0
total_lines = len(records)
line_n = 1
for record in records:
record: ADRLine
recording_time_sec_this_line: float = (record.time_budget_mins or 6.0) * 60.0
recording_time_sec = recording_time_sec + recording_time_sec_this_line
draw_header_block(c, cue_header_block, record)
# FIXME: Draw the title
# TODO: Integrate this report into the common DocTemplate api
# draw_title_box(c, title_header_block, record)
draw_character_row(c, char_row, record)
draw_cue_number_block(c, cue_number_block, record)
draw_timecode_block(
c, timecode_block, record, tc_display_format=tc_display_format
)
draw_reason_block(c, reason_block, record)
draw_prompt(c, prompt_row, prompt=record.prompt or "")
draw_notes(c, notes_row, note="")
draw_take_grid(c, take_grid_block)
draw_aux_block(c, aux_block, recording_time_sec_this_line, recording_time_sec)
draw_footer(
c, footer, record, report_date, line_no=line_n, total_lines=total_lines
)
line_n = line_n + 1
c.showPage()
c.save()
def output_report(lines, tc_display_format: TimecodeFormat):
report_date = datetime.datetime.now()
events = sorted(lines, key=lambda x: x.start)
character_numbers = set([x.character_id for x in lines])
for n in character_numbers:
create_report_for_character(
[e for e in events if e.character_id == n],
report_date,
tc_display_format=tc_display_format,
)
+89
View File
@@ -0,0 +1,89 @@
from typing import List
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import HRFlowable, KeepTogether, Paragraph, Spacer, Table
# from reportlab.pdfbase import pdfmetrics
# from reportlab.pdfbase.ttfonts import TTFont
from ..broadcast_timecode import TimecodeFormat
from ..docparser.adr_entity import ADRLine
from .__init__ import make_doc_template
def output_report(
lines: List[ADRLine], tc_display_format: TimecodeFormat, font_name="Helvetica"
):
character_numbers = set([n.character_id for n in lines])
# pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
for n in character_numbers:
char_lines = [
line for line in lines if not line.omitted and line.character_id == n
]
character_name = char_lines[0].character_name
char_lines = sorted(char_lines, key=lambda line: line.start)
title = "%s (%s) %s ADR Script" % (char_lines[0].title, character_name, n)
filename = "%s_%s_%s_ADR Script.pdf" % (char_lines[0].title, n, character_name)
doc = make_doc_template(
page_size=letter,
filename=filename,
document_title=title,
title=char_lines[0].title,
document_subheader=char_lines[0].spot or "",
supervisor=char_lines[0].supervisor or "",
client=char_lines[0].client or "",
document_header=character_name or "",
)
story = []
prompt_style = getSampleStyleSheet()["Normal"]
prompt_style.fontName = font_name
prompt_style.fontSize = 18.0
prompt_style.leading = 24.0
prompt_style.leftIndent = 1.5 * inch
prompt_style.rightIndent = 1.5 * inch
number_style = getSampleStyleSheet()["Normal"]
number_style.fontName = font_name
number_style.fontSize = 14
number_style.leading = 24
number_style.leftIndent = 0.0
number_style.rightIndent = 0.0
for line in char_lines:
start_tc = tc_display_format.seconds_to_smpte(line.start)
finish_tc = tc_display_format.seconds_to_smpte(line.finish)
data_block = [
[
Paragraph(line.cue_number, number_style),
Paragraph(start_tc + " - " + finish_tc, number_style),
]
]
# RIGHTWARDS ARROW →
# Unicode: U+2192, UTF-8: E2 86 92
story.append(
KeepTogether(
[
HRFlowable(width="50%", color=colors.black),
Table(
data=data_block,
colWidths=[1.5 * inch, 6.0 * inch],
style=[("LEFTPADDING", (0, 0), (-1, -1), 0.0)],
),
Paragraph(line.prompt, prompt_style),
Spacer(1.0, inch * 1.5),
]
)
)
doc.build(story)
+73
View File
@@ -0,0 +1,73 @@
"""
Reporting logic. These methods provide reporting methods to the package and
take some pains to provide nice-looking escape codes if we're writing to a
tty.
"""
import sys
def print_banner_style(message):
if sys.stderr.isatty():
sys.stderr.write(f"\n\033[1m{message}\033[0m\n\n")
else:
sys.stderr.write(f"\n{message}\n\n")
def print_section_header_style(message):
if sys.stderr.isatty():
sys.stderr.write(f"\n\033[4m{message}\033[0m\n\n")
else:
sys.stderr.write(f"{message}\n\n")
def print_status_style(message):
if sys.stderr.isatty():
sys.stderr.write(f"\033[3m - {message}\033[0m\n")
else:
sys.stderr.write(f" - {message}\n")
def print_warning(warning_string):
if sys.stderr.isatty():
sys.stderr.write(f"\033[3m - {warning_string}\033[0m\n")
else:
sys.stderr.write(f" - {warning_string}\n")
def print_advisory_tagging_error(
failed_string, position, parent_track_name=None, clip_time=None
):
if sys.stderr.isatty():
sys.stderr.write("\n")
sys.stderr.write(" ! \033[33;1mTagging error: \033[0m")
ok_string = failed_string[:position]
not_ok_string = failed_string[position:]
sys.stderr.write(
f'\033[32m"{ok_string}\033[31;1m{not_ok_string}"\033[0m\n'
)
if parent_track_name is not None:
sys.stderr.write(f' ! > On track "{parent_track_name}"\n')
if clip_time is not None:
sys.stderr.write(f" ! > In clip name at {clip_time}\n")
else:
sys.stderr.write("\n")
sys.stderr.write(f' ! Tagging error: "{failed_string}"\n')
sys.stderr.write(" ! %s _______________⬆\n" % (" " * position))
if parent_track_name is not None:
sys.stderr.write(f' ! > On track "{parent_track_name}"\n')
if clip_time is not None:
sys.stderr.write(f" ! > In clip name at {clip_time}\n")
sys.stderr.write("\n")
def print_fatal_error(message):
if sys.stderr.isatty():
sys.stderr.write(f"\n\033[5;31;1m*** {message} ***\033[0m\n")
else:
sys.stderr.write(f"\n{message}\n")
+88
View File
@@ -0,0 +1,88 @@
"""
Validation logic for enforcing various consistency rules.
"""
from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from ptulsconv.docparser.adr_entity import ADRLine
@dataclass
class ValidationError:
message: str
event: ADRLine | None = None
def report_message(self):
if self.event is not None:
return (
f"{self.message}: event at {self.event.start} with number"
"{self.event.cue_number}"
)
else:
return self.message
def validate_unique_count(input_lines: Iterator[ADRLine], field="title", count=1):
values = {getattr(e, field) for e in input_lines}
if len(values) > count:
yield ValidationError(
message=f"Field {field} has too many values (max={count}): {values}"
)
def validate_value(input_lines: Iterator[ADRLine], key_field, predicate):
for event in input_lines:
val = getattr(event, key_field)
if not predicate(val):
yield ValidationError(message=f"Field {val} not in range", event=event)
def validate_unique_field(
input_lines: Iterator[ADRLine], field="cue_number", scope=None
):
values = {}
for event in input_lines:
this = getattr(event, field)
if scope is not None:
key = getattr(event, scope)
else:
key = "_values"
values.setdefault(key, set())
if this in values[key]:
yield ValidationError(message=f"Re-used {field}", event=event)
else:
values[key].update(this)
def validate_non_empty_field(input_lines: Iterator[ADRLine], field="cue_number"):
for event in input_lines:
if getattr(event, field, None) is None:
yield ValidationError(message=f"Empty field {field}", event=event)
def validate_dependent_value(
input_lines: Iterator[ADRLine], key_field, dependent_field
):
"""
Validates that two events with the same value in `key_field` always have
the same value in `dependent_field`
"""
key_values = {getattr(x, key_field) for x in input_lines}
for key_value in key_values:
rows = [
(getattr(x, key_field), getattr(x, dependent_field))
for x in input_lines
if getattr(x, key_field) == key_value
]
unique_rows = set(rows)
if len(unique_rows) > 1:
message = f"Non-unique values for key {key_field} = "
for u in unique_rows:
message = message + f"\n - {u[0]} -> {u[1]}"
yield ValidationError(message=message, event=None)
+214
View File
@@ -0,0 +1,214 @@
from __future__ import annotations
import glob
import os
import os.path
import pathlib
import subprocess
import sys
from datetime import datetime
from importlib.metadata import version as module_version
from xml.etree.ElementTree import TreeBuilder, tostring
import ptulsconv
from ptulsconv.docparser.adr_entity import ADRLine
# TODO Get a third-party test for Avid Marker lists
def avid_marker_list(
lines: list[ADRLine],
report_date: datetime | None = None,
reel_start_frame=0,
fps=24,
):
doc = TreeBuilder(element_factory=None)
if report_date is None:
report_date = datetime.now()
doc.start("Avid:StreamItems", {"xmlns:Avid": "http://www.avid.com"})
doc.start("Avid:XMLFileData", {})
doc.start("AvProp", {"name": "DomainMagic", "type": "string"})
doc.data("Domain")
doc.end("AvProp")
doc.start("AvProp", {"name": "DomainKey", "type": "string"})
doc.data("58424a44")
doc.end("AvProp")
def insert_elem(kind, attb, atype, name, value):
doc.start("ListElem", {})
doc.start("AvProp", {"id": "ATTR", "name": "OMFI:ATTB:Kind", "type": "int32"})
doc.data(kind)
doc.end("AvProp")
doc.start("AvProp", {"id": "ATTR", "name": "OMFI:ATTB:Name", "type": "string"})
doc.data(name)
doc.end("AvProp")
doc.start("AvProp", {"id": "ATTR", "name": attb, "type": atype})
doc.data(value)
doc.end("AvProp")
doc.end("ListElem")
for line in lines:
doc.start("AvClass", {"id": "ATTR"})
doc.start(
"AvProp", {"id": "ATTR", "name": "__OMFI:ATTR:NumItems", "type": "int32"}
)
doc.data("7")
doc.end("AvProp")
doc.start("List", {"id": "OMFI:ATTR:AttrRefs"})
assert report_date
insert_elem(
"1",
"OMFI:ATTB:IntAttribute",
"int32",
"_ATN_CRM_LONG_CREATE_DATE",
report_date.strftime("%s"),
)
insert_elem(
"2", "OMFI:ATTB:StringAttribute", "string", "_ATN_CRM_COLOR", "yellow"
)
insert_elem(
"2",
"OMFI:ATTB:StringAttribute",
"string",
"_ATN_CRM_USER",
line.supervisor or "",
)
marker_name = f"{line.cue_number}: {line.prompt}"
insert_elem(
"2", "OMFI:ATTB:StringAttribute", "string", "_ATN_CRM_COM", marker_name
)
start_frame = int(line.start * fps)
insert_elem(
"2",
"OMFI:ATTB:StringAttribute",
"string",
"_ATN_CRM_TC",
str(start_frame - reel_start_frame),
)
insert_elem("2", "OMFI:ATTB:StringAttribute", "string", "_ATN_CRM_TRK", "V1")
insert_elem("1", "OMFI:ATTB:IntAttribute", "int32", "_ATN_CRM_LENGTH", "1")
doc.start("ListElem", {})
doc.end("ListElem")
doc.end("List")
doc.end("AvClass")
doc.end("Avid:XMLFileData")
doc.end("Avid:StreamItems")
def dump_fmpxml(data, input_file_name, output, adr_field_map):
doc = TreeBuilder(element_factory=None)
doc.start("FMPXMLRESULT", {"xmlns": "http://www.filemaker.com/fmpxmlresult"})
doc.start("ERRORCODE", {})
doc.data("0")
doc.end("ERRORCODE")
version = module_version("ptulsconv")
doc.start("PRODUCT", {"NAME": ptulsconv.__name__, "VERSION": f"{version}"})
doc.end("PRODUCT")
doc.start(
"DATABASE",
{
"DATEFORMAT": "MM/dd/yy",
"LAYOUT": "summary",
"TIMEFORMAT": "hh:mm:ss",
"RECORDS": str(len(data["events"])),
"NAME": os.path.basename(input_file_name),
},
)
doc.end("DATABASE")
doc.start("METADATA", {})
for field in adr_field_map:
tp = field[2]
ft = "TEXT"
if tp is int or tp is float:
ft = "NUMBER"
doc.start(
"FIELD", {"EMPTYOK": "YES", "MAXREPEAT": "1", "NAME": field[1], "TYPE": ft}
)
doc.end("FIELD")
doc.end("METADATA")
doc.start("RESULTSET", {"FOUND": str(len(data["events"]))})
for event in data["events"]:
doc.start("ROW", {})
for field in adr_field_map:
doc.start("COL", {})
doc.start("DATA", {})
for key_attempt in field[0]:
if key_attempt in event:
doc.data(str(event[key_attempt]))
break
doc.end("DATA")
doc.end("COL")
doc.end("ROW")
doc.end("RESULTSET")
doc.end("FMPXMLRESULT")
docelem = doc.close()
xmlstr = tostring(docelem, encoding="unicode", method="xml")
output.write(xmlstr)
xslt_path = os.path.join(pathlib.Path(__file__).parent.absolute(), "xslt")
def xform_options():
return glob.glob(os.path.join(xslt_path, "*.xsl"))
def dump_xform_options(output=sys.stdout):
print("# Available transforms:", file=output)
print(f"# Transform dir: {xslt_path}", file=output)
for f in xform_options():
base = os.path.basename(f)
name, _ = os.path.splitext(base)
print("# " + name, file=output)
def fmp_transformed_dump(data, input_file, xsl_name, output, adr_field_map):
import io
from ptulsconv.reporting import print_status_style
pipe = io.StringIO()
print_status_style("Generating base XML")
dump_fmpxml(data, input_file, pipe, adr_field_map)
str_data = pipe.getvalue()
print_status_style(f"Base XML size {len(str_data)}")
print_status_style("Running xsltproc")
xsl_path = os.path.join(
pathlib.Path(__file__).parent.absolute(), "xslt", xsl_name + ".xsl"
)
print_status_style(f"Using xsl: {xsl_path}")
subprocess.run(
["xsltproc", xsl_path, "-"],
input=str_data,
text=True,
stdout=output,
shell=False,
check=True,
)
+41 -18
View File
@@ -1,20 +1,21 @@
import unittest
from ptulsconv.docparser import parse_document
import os.path
import unittest
from ptulsconv.docparser import parse_document
class TestRobinHood1(unittest.TestCase):
path = os.path.dirname(__file__) + '/../export_cases/Robin Hood Spotting.txt'
path = os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting.txt"
def test_header_export(self):
with open(self.path, "r") as file:
session = parse_document(file.read())
self.assertIsNotNone(session.header)
self.assertEqual(session.header.session_name, 'Robin Hood Spotting')
self.assertEqual(session.header.session_name, "Robin Hood Spotting")
self.assertEqual(session.header.sample_rate, 48000.0)
self.assertEqual(session.header.bit_depth, 24)
self.assertEqual(session.header.timecode_fps, '29.97')
self.assertEqual(session.header.timecode_fps, "29.97")
self.assertEqual(session.header.timecode_drop_frame, False)
def test_all_sections(self):
@@ -35,12 +36,29 @@ class TestRobinHood1(unittest.TestCase):
session = parse_document(file.read())
self.assertEqual(len(session.tracks), 14)
self.assertListEqual(["Scenes", "Robin", "Will", "Marian", "John",
"Guy", "Much", "Butcher", "Town Crier",
"Soldier 1", "Soldier 2", "Soldier 3",
"Priest", "Guest at Court"],
list(map(lambda t: t.name, session.tracks)))
self.assertListEqual(["", "[ADR] {Actor=Errol Flynn} $CN=1",
self.assertListEqual(
[
"Scenes",
"Robin",
"Will",
"Marian",
"John",
"Guy",
"Much",
"Butcher",
"Town Crier",
"Soldier 1",
"Soldier 2",
"Soldier 3",
"Priest",
"Guest at Court",
],
list(map(lambda t: t.name, session.tracks)),
)
self.assertListEqual(
[
"",
"[ADR] {Actor=Errol Flynn} $CN=1",
"[ADR] {Actor=Patrick Knowles} $CN=2",
"[ADR] {Actor=Olivia DeHavilland} $CN=3",
"[ADR] {Actor=Claude Raines} $CN=4",
@@ -52,8 +70,10 @@ class TestRobinHood1(unittest.TestCase):
"[ADR] $CN=104",
"[ADR] $CN=105",
"[ADR] {Actor=Thomas R. Mills} $CN=106",
"[ADR] $CN=107"],
list(map(lambda t: t.comments, session.tracks)))
"[ADR] $CN=107",
],
list(map(lambda t: t.comments, session.tracks)),
)
def test_a_track(self):
@@ -61,19 +81,22 @@ class TestRobinHood1(unittest.TestCase):
session = parse_document(file.read())
guy_track = session.tracks[5]
self.assertEqual(guy_track.name, 'Guy')
self.assertEqual(guy_track.comments, '[ADR] {Actor=Basil Rathbone} $CN=5')
self.assertEqual(guy_track.name, "Guy")
self.assertEqual(guy_track.comments, "[ADR] {Actor=Basil Rathbone} $CN=5")
self.assertEqual(guy_track.user_delay_samples, 0)
self.assertListEqual(guy_track.state, [])
self.assertEqual(len(guy_track.clips), 16)
self.assertEqual(guy_track.clips[5].channel, 1)
self.assertEqual(guy_track.clips[5].event, 6)
self.assertEqual(guy_track.clips[5].clip_name, "\"What's your name? You Saxon dog!\" $QN=GY106")
self.assertEqual(
guy_track.clips[5].clip_name,
'"What\'s your name? You Saxon dog!" $QN=GY106',
)
self.assertEqual(guy_track.clips[5].start_timecode, "01:04:19:15")
self.assertEqual(guy_track.clips[5].finish_timecode, "01:04:21:28")
self.assertEqual(guy_track.clips[5].duration, "00:00:02:13")
self.assertEqual(guy_track.clips[5].timestamp, None)
self.assertEqual(guy_track.clips[5].state, 'Unmuted')
self.assertEqual(guy_track.clips[5].state, "Unmuted")
def test_memory_locations(self):
with open(self.path, "r") as file:
@@ -86,5 +109,5 @@ class TestRobinHood1(unittest.TestCase):
self.assertEqual(session.markers[0].units, "Samples")
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()
+16 -12
View File
@@ -1,10 +1,11 @@
import unittest
from ptulsconv.docparser import parse_document
import os.path
import unittest
from ptulsconv.docparser import parse_document
class TestRobinHood5(unittest.TestCase):
path = os.path.dirname(__file__) + '/../export_cases/Robin Hood Spotting5.txt'
path = os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting5.txt"
def test_skipped_segments(self):
with open(self.path, "r") as file:
@@ -23,30 +24,33 @@ class TestRobinHood5(unittest.TestCase):
with open(self.path, "r") as file:
session = parse_document(file.read())
self.assertEqual(session.tracks[1].name, 'MX WT (Stereo)')
self.assertEqual(session.tracks[1].name, "MX WT (Stereo)")
self.assertEqual(len(session.tracks[1].clips), 2)
self.assertEqual(session.tracks[1].clips[0].clip_name, 'RobinHood.1-01.L')
self.assertEqual(session.tracks[1].clips[1].clip_name, 'RobinHood.1-01.R')
self.assertEqual(session.tracks[1].clips[0].clip_name, "RobinHood.1-01.L")
self.assertEqual(session.tracks[1].clips[1].clip_name, "RobinHood.1-01.R")
def test_a_track(self):
with open(self.path, "r") as file:
session = parse_document(file.read())
guy_track = session.tracks[8]
self.assertEqual(guy_track.name, 'Guy')
self.assertEqual(guy_track.comments, '[ADR] {Actor=Basil Rathbone} $CN=5')
self.assertEqual(guy_track.name, "Guy")
self.assertEqual(guy_track.comments, "[ADR] {Actor=Basil Rathbone} $CN=5")
self.assertEqual(guy_track.user_delay_samples, 0)
self.assertListEqual(guy_track.state, ['Solo'])
self.assertListEqual(guy_track.state, ["Solo"])
self.assertEqual(len(guy_track.clips), 16)
self.assertEqual(guy_track.clips[5].channel, 1)
self.assertEqual(guy_track.clips[5].event, 6)
self.assertEqual(guy_track.clips[5].clip_name, "\"What's your name? You Saxon dog!\" $QN=GY106")
self.assertEqual(
guy_track.clips[5].clip_name,
'"What\'s your name? You Saxon dog!" $QN=GY106',
)
self.assertEqual(guy_track.clips[5].start_timecode, "01:04:19:15.00")
self.assertEqual(guy_track.clips[5].finish_timecode, "01:04:21:28.00")
self.assertEqual(guy_track.clips[5].duration, "00:00:02:13.00")
self.assertEqual(guy_track.clips[5].timestamp, "01:04:19:09.70")
self.assertEqual(guy_track.clips[5].state, 'Unmuted')
self.assertEqual(guy_track.clips[5].state, "Unmuted")
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()
+18 -12
View File
@@ -1,33 +1,39 @@
import unittest
from ptulsconv.docparser import parse_document
import os.path
import unittest
from ptulsconv.docparser import parse_document
class TestRobinHood6(unittest.TestCase):
path = os.path.dirname(__file__) + '/../export_cases/Robin Hood Spotting6.txt'
path = os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting6.txt"
def test_a_track(self):
with open(self.path, "r") as file:
session = parse_document(file.read())
marian_track = session.tracks[6]
self.assertEqual(marian_track.name, 'Marian')
self.assertEqual(marian_track.comments, '[ADR] {Actor=Olivia DeHavilland} $CN=3')
self.assertEqual(marian_track.name, "Marian")
self.assertEqual(
marian_track.comments, "[ADR] {Actor=Olivia DeHavilland} $CN=3"
)
self.assertEqual(marian_track.user_delay_samples, 0)
self.assertListEqual(marian_track.state, ['Solo'])
self.assertListEqual(marian_track.state, ["Solo"])
self.assertEqual(len(marian_track.clips), 4)
self.assertListEqual(marian_track.plugins, ['Channel Strip (mono)', 'ReVibe II (mono/5.1)'])
self.assertListEqual(
marian_track.plugins, ["Channel Strip (mono)", "ReVibe II (mono/5.1)"]
)
self.assertEqual(marian_track.clips[2].channel, 1)
self.assertEqual(marian_track.clips[2].event, 3)
self.assertEqual(marian_track.clips[2].clip_name,
"\"Isn't that reason enough for a Royal Ward who must obey her guardian?\" $QN=M103")
self.assertEqual(
marian_track.clips[2].clip_name,
'"Isn\'t that reason enough for a Royal Ward who must obey her guardian?" $QN=M103',
)
self.assertEqual(marian_track.clips[2].start_timecode, "01:08:01:11")
self.assertEqual(marian_track.clips[2].finish_timecode, "01:08:04:24")
self.assertEqual(marian_track.clips[2].duration, "00:00:03:12")
self.assertEqual(marian_track.clips[2].timestamp, "01:08:01:11")
self.assertEqual(marian_track.clips[2].state, 'Unmuted')
self.assertEqual(marian_track.clips[2].state, "Unmuted")
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()
+10 -7
View File
@@ -1,10 +1,11 @@
import unittest
from ptulsconv.docparser import parse_document
import os.path
import unittest
from ptulsconv.docparser import parse_document
class TestRobinHoodDF(unittest.TestCase):
path = os.path.dirname(__file__) + '/../export_cases/Robin Hood SpottingDF.txt'
path = os.path.dirname(__file__) + "/../export_cases/Robin Hood SpottingDF.txt"
def test_header_export_df(self):
@@ -19,16 +20,18 @@ class TestRobinHoodDF(unittest.TestCase):
session = parse_document(file.read())
guy_track = session.tracks[4]
self.assertEqual(guy_track.name, 'Robin')
self.assertEqual(guy_track.comments, '[ADR] {Actor=Errol Flynn} $CN=1')
self.assertEqual(guy_track.name, "Robin")
self.assertEqual(guy_track.comments, "[ADR] {Actor=Errol Flynn} $CN=1")
self.assertEqual(guy_track.user_delay_samples, 0)
self.assertListEqual(guy_track.state, [])
self.assertEqual(len(guy_track.clips), 10)
self.assertEqual(guy_track.clips[5].channel, 1)
self.assertEqual(guy_track.clips[5].event, 6)
self.assertEqual(guy_track.clips[5].clip_name, "\"Hold there! What's his fault?\" $QN=R106")
self.assertEqual(
guy_track.clips[5].clip_name, '"Hold there! What\'s his fault?" $QN=R106'
)
self.assertEqual(guy_track.clips[5].start_timecode, "01:05:30;15")
self.assertEqual(guy_track.clips[5].finish_timecode, "01:05:32;01")
self.assertEqual(guy_track.clips[5].duration, "00:00:01;16")
self.assertEqual(guy_track.clips[5].timestamp, None)
self.assertEqual(guy_track.clips[5].state, 'Unmuted')
self.assertEqual(guy_track.clips[5].state, "Unmuted")
+10 -13
View File
@@ -1,11 +1,8 @@
import os
import os.path
import tempfile
import unittest
import tempfile
import sys
import os.path
import os
import glob
from ptulsconv import commands
@@ -16,13 +13,12 @@ class TestPDFExport(unittest.TestCase):
be converted into PDF docs without throwing an error
"""
files = []
files = [os.path.dirname(__file__) +
"/../export_cases/Robin Hood Spotting.txt"]
files = [os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting.txt"]
for path in files:
tempdir = tempfile.TemporaryDirectory()
os.chdir(tempdir.name)
try:
commands.convert(input_file=path, major_mode='doc')
commands.convert(input_file=path, major_mode="doc")
except Exception as e:
print("Error in test_report_generation")
print(f"File: {path}")
@@ -33,13 +29,14 @@ class TestPDFExport(unittest.TestCase):
def test_report_generation_track_markers(self):
files = []
files.append(os.path.dirname(__file__) +
"/../export_cases/Test for ptulsconv.txt")
files.append(
os.path.dirname(__file__) + "/../export_cases/Test for ptulsconv.txt"
)
for path in files:
tempdir = tempfile.TemporaryDirectory()
os.chdir(tempdir.name)
try:
commands.convert(input_file=path, major_mode='doc')
commands.convert(input_file=path, major_mode="doc")
except Exception as e:
print("Error in test_report_generation_track_markers")
print(f"File: {path}")
@@ -49,5 +46,5 @@ class TestPDFExport(unittest.TestCase):
tempdir.cleanup()
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()
+21 -18
View File
@@ -1,38 +1,41 @@
import unittest
from ptulsconv.docparser.tag_compiler import Event
from ptulsconv.docparser.adr_entity import ADRLine, make_entity
from fractions import Fraction
from ptulsconv.docparser.adr_entity import ADRLine, make_entity
from ptulsconv.docparser.tag_compiler import Event
class TestADREntity(unittest.TestCase):
def test_event2line(self):
tags = {
'Ver': '1.0',
'Actor': "Bill",
'CN': "1",
'QN': 'J1001',
'R': 'Noise',
'EFF': 'EFF'
"Ver": "1.0",
"Actor": "Bill",
"CN": "1",
"QN": "J1001",
"R": "Noise",
"EFF": "EFF",
}
event = Event(clip_name='"This is a test." (sotto voce)',
event = Event(
clip_name='"This is a test." (sotto voce)',
track_name="Justin",
session_name="Test Project",
tags=tags,
start=Fraction(0, 1), finish=Fraction(1, 1))
start=Fraction(0, 1),
finish=Fraction(1, 1),
)
line = make_entity(event)
self.assertIsInstance(line, ADRLine)
self.assertEqual('Bill', line.actor_name)
self.assertEqual('Justin', line.character_name)
self.assertEqual("Bill", line.actor_name)
self.assertEqual("Justin", line.character_name)
self.assertEqual('"This is a test." (sotto voce)', line.prompt)
self.assertEqual('Noise', line.reason)
self.assertEqual('J1001', line.cue_number)
self.assertEqual("Noise", line.reason)
self.assertEqual("J1001", line.cue_number)
self.assertEqual(True, line.effort)
self.assertEqual('Test Project', line.title)
self.assertEqual('1.0', line.version)
self.assertEqual("Test Project", line.title)
self.assertEqual("1.0", line.version)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()
+10 -4
View File
@@ -1,7 +1,9 @@
import unittest
from ptulsconv import broadcast_timecode
from fractions import Fraction
from ptulsconv import broadcast_timecode
class TestBroadcastTimecode(unittest.TestCase):
def test_basic_to_frame_count(self):
r1 = "01:00:00:00"
@@ -66,7 +68,9 @@ class TestBroadcastTimecode(unittest.TestCase):
def test_seconds_to_smpte(self):
secs = Fraction(25, 24)
frame_duration = Fraction(1, 24)
tc_format = broadcast_timecode.TimecodeFormat(frame_duration=frame_duration, logical_fps=24, drop_frame=False)
tc_format = broadcast_timecode.TimecodeFormat(
frame_duration=frame_duration, logical_fps=24, drop_frame=False
)
s1 = tc_format.seconds_to_smpte(secs)
self.assertEqual(s1, "00:00:01:01")
@@ -77,9 +81,11 @@ class TestBroadcastTimecode(unittest.TestCase):
def test_unparseable_timecode(self):
time_str = "11.32-19"
s1 = broadcast_timecode.smpte_to_frame_count(time_str, frames_per_logical_second=24)
s1 = broadcast_timecode.smpte_to_frame_count(
time_str, frames_per_logical_second=24
)
self.assertIsNone(s1)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()
+7 -5
View File
@@ -1,12 +1,13 @@
import unittest
from ptulsconv.docparser.doc_entity import HeaderDescriptor
from fractions import Fraction
from ptulsconv.docparser.doc_entity import HeaderDescriptor
class DocParserTestCase(unittest.TestCase):
def test_header(self):
header = HeaderDescriptor(session_name="Test Session",
header = HeaderDescriptor(
session_name="Test Session",
sample_rate=48000.0,
bit_depth=24,
start_timecode="00:59:52:00",
@@ -14,11 +15,12 @@ class DocParserTestCase(unittest.TestCase):
timecode_drop_frame=False,
count_audio_tracks=0,
count_clips=0,
count_files=0)
count_files=0,
)
self.assertEqual(header.session_name, "Test Session")
self.assertEqual(header.start_time, Fraction((59 * 60 + 52) * 30, 30))
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()
+2 -2
View File
@@ -1,6 +1,8 @@
import unittest
from ptulsconv import footage
class TestFootage(unittest.TestCase):
def test_basic_footage(self):
r1 = "90+0"
@@ -11,5 +13,3 @@ class TestFootage(unittest.TestCase):
r1 = "1+8"
f1 = footage.footage_to_seconds(r1)
self.assertEqual(float(f1 or 0), 1.0)
+90 -56
View File
@@ -1,12 +1,11 @@
import unittest
from fractions import Fraction
import ptulsconv.docparser.tag_compiler
from ptulsconv.docparser import doc_entity
from fractions import Fraction
class TestTagCompiler(unittest.TestCase):
def test_one_track(self):
c = ptulsconv.docparser.tag_compiler.TagCompiler()
@@ -16,31 +15,48 @@ class TestTagCompiler(unittest.TestCase):
events = c.compile_events()
event1 = next(events)
self.assertEqual('This is clip 1', event1.clip_name)
self.assertEqual('Track 1', event1.track_name)
self.assertEqual('Test Session', event1.session_name)
self.assertEqual(dict(A='A',
Color='Blue',
Ver='1.1',
Mode='2',
Comment='This is some text in the comments',
Part='1'), event1.tags)
self.assertEqual("This is clip 1", event1.clip_name)
self.assertEqual("Track 1", event1.track_name)
self.assertEqual("Test Session", event1.session_name)
self.assertEqual(
dict(
A="A",
Color="Blue",
Ver="1.1",
Mode="2",
Comment="This is some text in the comments",
Part="1",
),
event1.tags,
)
self.assertEqual(Fraction(3600, 1), event1.start)
event2 = next(events)
self.assertEqual("This is the second clip ...and this is the last clip", event2.clip_name)
self.assertEqual('Track 1', event2.track_name)
self.assertEqual('Test Session', event2.session_name)
self.assertEqual(dict(R='Noise', A='A', B='B',
Color='Red',
Comment='This is some text in the comments',
N='1', Mode='2',
Ver='1.1',
M1='M1',
Part='2'), event2.tags)
self.assertEqual(
"This is the second clip ...and this is the last clip", event2.clip_name
)
self.assertEqual("Track 1", event2.track_name)
self.assertEqual("Test Session", event2.session_name)
self.assertEqual(
dict(
R="Noise",
A="A",
B="B",
Color="Red",
Comment="This is some text in the comments",
N="1",
Mode="2",
Ver="1.1",
M1="M1",
Part="2",
),
event2.tags,
)
self.assertEqual(c.session.header.convert_timecode('01:00:01:10'), event2.start)
self.assertEqual(c.session.header.convert_timecode('01:00:03:00'), event2.finish)
self.assertEqual(c.session.header.convert_timecode("01:00:01:10"), event2.start)
self.assertEqual(
c.session.header.convert_timecode("01:00:03:00"), event2.finish
)
self.assertIsNone(next(events, None))
@@ -51,42 +67,52 @@ class TestTagCompiler(unittest.TestCase):
all_tags = c.compile_tag_list()
self.assertTrue(all_tags['Mode'] == {'2', '1'})
self.assertTrue(all_tags["Mode"] == {"2", "1"})
@staticmethod
def make_test_session():
test_header = doc_entity.HeaderDescriptor(session_name="Test Session $Ver=1.1",
test_header = doc_entity.HeaderDescriptor(
session_name="Test Session $Ver=1.1",
sample_rate=48000,
timecode_format="24",
timecode_drop_frame=False,
bit_depth=24,
start_timecode='00:59:00:00',
start_timecode="00:59:00:00",
count_audio_tracks=1,
count_clips=3,
count_files=0
count_files=0,
)
test_clips = [
doc_entity.TrackClipDescriptor(channel=1, event=1,
clip_name='This is clip 1 {Color=Blue} $Mode=2',
start_time='01:00:00:00',
finish_time='01:00:01:03',
duration='00:00:01:03',
state='Unmuted',
timestamp=None),
doc_entity.TrackClipDescriptor(channel=1, event=2,
clip_name='This is the second clip {R=Noise} [B] $Mode=2',
start_time='01:00:01:10',
finish_time='01:00:02:00',
duration='00:00:00:14',
state='Unmuted',
timestamp=None),
doc_entity.TrackClipDescriptor(channel=1, event=3,
clip_name='& ...and this is the last clip $N=1 $Mode=2',
start_time='01:00:02:00',
finish_time='01:00:03:00',
duration='00:00:01:00',
state='Unmuted',
timestamp=None),
doc_entity.TrackClipDescriptor(
channel=1,
event=1,
clip_name="This is clip 1 {Color=Blue} $Mode=2",
start_time="01:00:00:00",
finish_time="01:00:01:03",
duration="00:00:01:03",
state="Unmuted",
timestamp=None,
),
doc_entity.TrackClipDescriptor(
channel=1,
event=2,
clip_name="This is the second clip {R=Noise} [B] $Mode=2",
start_time="01:00:01:10",
finish_time="01:00:02:00",
duration="00:00:00:14",
state="Unmuted",
timestamp=None,
),
doc_entity.TrackClipDescriptor(
channel=1,
event=3,
clip_name="& ...and this is the last clip $N=1 $Mode=2",
start_time="01:00:02:00",
finish_time="01:00:03:00",
duration="00:00:01:00",
state="Unmuted",
timestamp=None,
),
]
test_track = doc_entity.TrackDescriptor(
index=0,
@@ -95,32 +121,40 @@ class TestTagCompiler(unittest.TestCase):
user_delay_samples=0,
plugins=[],
state=[],
clips=test_clips)
clips=test_clips,
)
markers = [doc_entity.MarkerDescriptor(number=1,
markers = [
doc_entity.MarkerDescriptor(
number=1,
location="01:00:00:00",
time_reference=48000 * 3600,
units="Samples",
name="Marker 1 {Part=1}",
comments="", track_marker=False,
comments="",
track_marker=False,
),
doc_entity.MarkerDescriptor(number=2,
doc_entity.MarkerDescriptor(
number=2,
location="01:00:01:00",
time_reference=48000 * 3601,
units="Samples",
name="Marker 2 {Part=2}",
comments="[M1]", track_marker=False,
comments="[M1]",
track_marker=False,
),
]
test_session = doc_entity.SessionDescriptor(header=test_header,
test_session = doc_entity.SessionDescriptor(
header=test_header,
tracks=[test_track],
clips=[],
files=[],
markers=markers,
plugins=[])
plugins=[],
)
return test_session
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()
+6 -6
View File
@@ -1,6 +1,6 @@
import unittest
from ptulsconv.docparser.tagged_string_parser_visitor import parse_tags, TagPreModes
from ptulsconv.docparser.tagged_string_parser_visitor import TagPreModes, parse_tags
class TestTagInterpreter(unittest.TestCase):
@@ -17,12 +17,12 @@ class TestTagInterpreter(unittest.TestCase):
def test_tags(self):
s1 = parse_tags("{a=100}")
self.assertEqual(s1.tag_dict['a'], "100")
self.assertEqual(s1.tag_dict["a"], "100")
s2 = parse_tags("{b=This is a test} [option] $X=9")
self.assertEqual(s2.tag_dict['b'], 'This is a test')
self.assertEqual(s2.tag_dict['option'], 'option')
self.assertEqual(s2.tag_dict['X'], "9")
self.assertEqual(s2.tag_dict["b"], "This is a test")
self.assertEqual(s2.tag_dict["option"], "option")
self.assertEqual(s2.tag_dict["X"], "9")
def test_modes(self):
s1 = parse_tags("@ Monday Tuesday {a=1}")
@@ -35,5 +35,5 @@ class TestTagInterpreter(unittest.TestCase):
self.assertEqual(s3.mode, TagPreModes.APPEND)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()
+35 -20
View File
@@ -1,15 +1,18 @@
import unittest
from ptulsconv.docparser import doc_entity, pt_doc_parser, tag_compiler
import os.path
import unittest
from ptulsconv.docparser import doc_entity, pt_doc_parser, tag_compiler
class TaggingIntegratedTests(unittest.TestCase):
path = os.path.dirname(__file__) + '/../export_cases/Tag Tests/Tag Tests.txt'
path = os.path.dirname(__file__) + "/../export_cases/Tag Tests/Tag Tests.txt"
def test_event_list(self):
with open(self.path, 'r') as f:
with open(self.path, "r") as f:
document_ast = pt_doc_parser.protools_text_export_grammar.parse(f.read())
document: doc_entity.SessionDescriptor = pt_doc_parser.DocParserVisitor().visit(document_ast)
document: doc_entity.SessionDescriptor = (
pt_doc_parser.DocParserVisitor().visit(document_ast)
)
compiler = tag_compiler.TagCompiler()
compiler.session = document
@@ -27,9 +30,11 @@ class TaggingIntegratedTests(unittest.TestCase):
self.assertEqual("Region 04", events[8].clip_name)
def test_append(self):
with open(self.path, 'r') as f:
with open(self.path, "r") as f:
document_ast = pt_doc_parser.protools_text_export_grammar.parse(f.read())
document: doc_entity.SessionDescriptor = pt_doc_parser.DocParserVisitor().visit(document_ast)
document: doc_entity.SessionDescriptor = (
pt_doc_parser.DocParserVisitor().visit(document_ast)
)
compiler = tag_compiler.TagCompiler()
compiler.session = document
@@ -39,20 +44,26 @@ class TaggingIntegratedTests(unittest.TestCase):
self.assertEqual("Dolor sic amet the rain in spain", events[2].clip_name)
self.assertEqual(document.header.convert_timecode("01:00:10:00"), events[2].start)
self.assertEqual(document.header.convert_timecode("01:00:25:00"), events[2].finish)
self.assertEqual(
document.header.convert_timecode("01:00:10:00"), events[2].start
)
self.assertEqual(
document.header.convert_timecode("01:00:25:00"), events[2].finish
)
self.assertIn('X', events[2].tags.keys())
self.assertIn('ABC', events[2].tags.keys())
self.assertIn('A', events[2].tags.keys())
self.assertEqual('302', events[2].tags['X'])
self.assertEqual('ABC', events[2].tags['ABC'])
self.assertEqual('1', events[2].tags['A'])
self.assertIn("X", events[2].tags.keys())
self.assertIn("ABC", events[2].tags.keys())
self.assertIn("A", events[2].tags.keys())
self.assertEqual("302", events[2].tags["X"])
self.assertEqual("ABC", events[2].tags["ABC"])
self.assertEqual("1", events[2].tags["A"])
def test_successive_appends(self):
with open(self.path, 'r') as f:
with open(self.path, "r") as f:
document_ast = pt_doc_parser.protools_text_export_grammar.parse(f.read())
document: doc_entity.SessionDescriptor = pt_doc_parser.DocParserVisitor().visit(document_ast)
document: doc_entity.SessionDescriptor = (
pt_doc_parser.DocParserVisitor().visit(document_ast)
)
compiler = tag_compiler.TagCompiler()
compiler.session = document
@@ -62,9 +73,13 @@ class TaggingIntegratedTests(unittest.TestCase):
self.assertEqual("A B C", events[3].clip_name)
self.assertEqual(document.header.convert_timecode("01:00:15:00"), events[3].start)
self.assertEqual(document.header.convert_timecode("01:00:45:00"), events[3].finish)
self.assertEqual(
document.header.convert_timecode("01:00:15:00"), events[3].start
)
self.assertEqual(
document.header.convert_timecode("01:00:45:00"), events[3].finish
)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()
+3 -3
View File
@@ -8,13 +8,13 @@ class MyTestCase(unittest.TestCase):
v = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
expected = [1, 2, 7, 5, 6, 15, 9, 10]
should = (lambda x, y: y % 4 == 0)
do_combine = (lambda x, y: x + y)
should = lambda x, y: y % 4 == 0
do_combine = lambda x, y: x + y
r = apply_appends(iter(v), should, do_combine)
r1 = list(r)
self.assertEqual(r1, expected)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()