Compare commits

..

7 Commits

Author SHA1 Message Date
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
20 changed files with 278 additions and 215 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v2.5.0
+22
View File
@@ -0,0 +1,22 @@
name: Test Toot
on:
workflow_dispatch:
jobs:
print-tag:
runs-on: ubuntu-latest
steps:
- name: Report to Mastodon
uses: cbrgm/mastodon-github-action@v1.0.1
env:
MASTODON_URL: ${{ secrets.MASTODON_URL }}
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
with:
message: |
This is a test toot, automatically posted by a github action.
${{ github.server_url }}/${{ github.repository }}
${{ github.ref }}
+16
View File
@@ -14,6 +14,9 @@ build:
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`
@@ -26,3 +29,16 @@ sphinx:
#If using Sphinx, optionally build your docs in additional formats such as PDF
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
+2 -9
View File
@@ -1,6 +1,6 @@
[project]
name = "ptulsconv"
version = "4.0.2"
version = "4.0.0"
description = "Read Pro Tools Text exports and generate PDF ADR Reports, JSON"
readme = "README.md"
requires-python = ">=3.9"
@@ -9,6 +9,7 @@ license-files = ["LICENSE"]
keywords = ["text-processing", "parsers", "film",
"broadcast", "editing", "editorial"]
classifiers = [
'License :: OSI Approved :: MIT License',
'Topic :: Multimedia',
'Topic :: Multimedia :: Sound/Audio',
"Programming Language :: Python :: 3.9",
@@ -16,7 +17,6 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Development Status :: 5 - Production/Stable",
"Topic :: Text Processing :: Filters"
]
@@ -42,13 +42,6 @@ doc = [
"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"
+15 -10
View File
@@ -1,20 +1,20 @@
# type: ignore[reportUndefinedVariable]
# Export Items as Text.py
# (c) 2021 Jamie Hardt. All rights reserved.
#
#
import datetime
import json
from zoneinfo import ZoneInfo
item_records = []
item_records = list()
for i in range(RPR_CountMediaItems(0)):
this_item = RPR_GetMediaItem(0, i)
item_record = {}
item_record["mute"] = RPR_GetMediaItemInfo_Value(this_item, "B_MUTE_ACTUAL") > 0.0
item_record["mute"] = (
True if RPR_GetMediaItemInfo_Value(this_item, "B_MUTE_ACTUAL") > 0.0 else False
)
item_record["duration"] = RPR_GetMediaItemInfo_Value(this_item, "D_LENGTH")
_, item_record["duration_tc"], _, _, _ = RPR_format_timestr_len(
@@ -26,7 +26,9 @@ for i in range(RPR_CountMediaItems(0)):
item_record["position"], "", 128, 5
)
item_record["selected"] = RPR_GetMediaItemInfo_Value(this_item, "B_UISEL") > 0.0
item_record["selected"] = (
True if RPR_GetMediaItemInfo_Value(this_item, "B_UISEL") > 0.0 else False
)
_, _, _, item_record["notes"], _ = RPR_GetSetMediaItemInfo_String(
this_item, "P_NOTES", "", False
)
@@ -52,11 +54,13 @@ for i in range(RPR_CountMediaItems(0)):
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_record["track_muted"] = (
True if RPR_GetMediaTrackInfo_Value(item_track, "B_MUTE") > 0.0 else False
)
item_records = item_records + [item_record]
output = {}
output = dict()
output["items"] = item_records
_, output["project_title"], _ = RPR_GetProjectName(0, "", 1024)
_, _, output["project_author"], _ = RPR_GetSetProjectAuthor(0, False, "", 1024)
@@ -72,14 +76,15 @@ output_title = output["project_title"]
if output_title == "":
output_title = "unsaved project"
output_file_name = f"{output_title} Text Export {now.strftime('%Y%m%d_%H%M')}.txt"
output_file_name = "%s Text Export %s.txt" % (output_title, now.strftime("%Y%m%d_%H%M"))
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(
f'Exported text file "{output_file_name}" to project folder. Text Export Complete',
'Exported text file "%s" to project folder.' % output_file_name,
"Text Export Complete",
0,
)
+23 -9
View File
@@ -9,6 +9,7 @@ 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,
)
@@ -101,7 +102,7 @@ def main():
(options, args) = parser.parse_args(sys.argv)
print_section_header_style("Startup")
print_status_style(f"This run started {datetime.datetime.now().isoformat()}")
print_status_style("This run started %s" % (datetime.datetime.now().isoformat()))
if options.show_tags:
dump_field_map()
@@ -110,16 +111,29 @@ def main():
elif options.show_formats:
dump_formats()
sys.exit(0)
try:
major_mode = options.output_format
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 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__":
+20 -24
View File
@@ -2,24 +2,21 @@
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 NamedTuple
from typing import Optional, SupportsFloat
class TimecodeFormat(NamedTuple):
class TimecodeFormat(
namedtuple("_TimecodeFormat", "frame_duration logical_fps drop_frame")
):
"""
A struct reperesenting a timecode datum.
"""
frame_duration: Fraction
logical_fps: int
drop_frame: bool
def smpte_to_seconds(self, smpte: str) -> Fraction | None:
def smpte_to_seconds(self, smpte: str) -> Optional[Fraction]:
frame_count = smpte_to_frame_count(
smpte, self.logical_fps, drop_frame_hint=self.drop_frame
)
@@ -28,14 +25,14 @@ class TimecodeFormat(NamedTuple):
else:
return frame_count * self.frame_duration
def seconds_to_smpte(self, seconds: Fraction) -> str:
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)
def smpte_to_frame_count(
smpte_rep_string: str, frames_per_logical_second: int, drop_frame_hint=False
) -> int | None:
) -> Optional[int]:
"""
Convert a string with a SMPTE timecode representation into a frame count.
@@ -86,7 +83,7 @@ def frame_count_to_smpte(
frame_count: int,
frames_per_logical_second: int,
drop_frame: bool = False,
fractional_frame: float | None = None,
fractional_frame: Optional[float] = None,
) -> str:
assert frames_per_logical_second in [24, 25, 30, 48, 50, 60]
assert fractional_frame is None or fractional_frame < 1.0
@@ -107,21 +104,20 @@ def frame_count_to_smpte(
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:
fpart = "{fractional_frame:.3f}"[1:]
return f"{hh:02}:{mm:02}:{ss:02}{separator}{ff:02}{fpart}"
return "%02i:%02i:%02i%s%02i%s" % (
hh,
mm,
ss,
separator,
ff,
("%.3f" % fractional_frame)[1:],
)
else:
return f"{hh:02}:{mm:02}:{ss:02}{separator}{ff:02}"
# "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff)
return "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff)
def footage_to_frame_count(footage_string) -> int | None:
def footage_to_frame_count(footage_string) -> Optional[int]:
m = re.search(r"(\d+)\+(\d+)(\.\d+)?", footage_string)
if m is None:
return None
@@ -135,4 +131,4 @@ def footage_to_frame_count(footage_string) -> int | None:
def frame_count_to_footage(frame_count):
feet, frm = divmod(frame_count, 16)
return f"{feet}+{frm:02}"
return "%i+%02i" % (feet, frm)
+21 -18
View File
@@ -3,8 +3,6 @@ This module provides the main input document parsing and transform
implementation.
"""
from __future__ import annotations
import csv
import datetime
import os
@@ -13,6 +11,7 @@ from collections.abc import Iterator
from fractions import Fraction
from itertools import chain
from json import JSONEncoder
from typing import List, Optional
import ptsl
@@ -40,25 +39,26 @@ class FractionEncoder(JSONEncoder):
as a dict.
"""
force_denominator: int | None
force_denominator: Optional[int]
def default(self, o):
""" """
if isinstance(o, Fraction):
return {"numerator": o.numerator, "denominator": o.denominator}
return dict(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[str | None] = {ln.reel for ln in lines}
reels: set[str | None] = set([ln.reel for ln in lines])
for n, name in [(n.character_id, n.character_name) for n in lines]:
dir_name = f"{n}_{name}"
dir_name = "%s_%s" % (n, name)
os.makedirs(dir_name, exist_ok=True)
os.chdir(dir_name)
for reel in reels:
@@ -69,8 +69,11 @@ def output_adr_csv(lines: list[ADRLine], time_format: TimecodeFormat):
if len(these_lines) == 0:
continue
outfile_name = (
f"{these_lines[0].title}_{n}_{these_lines[0].character_name}_{reel}.csv"
outfile_name = "%s_%s_%s_%s.csv" % (
these_lines[0].title,
n,
these_lines[0].character_name,
reel,
)
with open(outfile_name, mode="w", newline="") as outfile:
@@ -116,13 +119,13 @@ def output_adr_csv(lines: list[ADRLine], time_format: TimecodeFormat):
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 = f"{title}_{report_date.strftime('%Y-%m-%d_%H%M%S')}"
reports_dir = "%s_%s" % (title, report_date.strftime("%Y-%m-%d_%H%M%S"))
os.makedirs(reports_dir, exist_ok=False)
os.chdir(reports_dir)
@@ -152,7 +155,7 @@ def generate_documents(session_tc_format, scenes, adr_lines: list[ADRLine], titl
def create_adr_reports(
lines: list[ADRLine], tc_display_format: TimecodeFormat, reel_list: list[str]
lines: List[ADRLine], tc_display_format: TimecodeFormat, reel_list: List[str]
):
"""
Creates a directory heirarchy and a respective set of ADR reports,
@@ -237,18 +240,18 @@ def convert(major_mode, input_file=None, output=sys.stdout, warnings=True):
)
# TODO: Breakdown by titles
titles = {x.title for x in (generic_events + adr_lines)}
titles = set([x.title for x in (generic_events + adr_lines)])
if len(titles) != 1:
print_warning(
"Multiple titles per export is not supported, "
f"found multiple titles: {titles} Exiting."
"found multiple titles: %s Exiting." % titles
)
sys.exit(-1)
exit(-1)
title = next(iter(titles))
title = list(titles)[0]
print_status_style(f"{len(generic_events)} generic events found.")
print_status_style(f"{len(adr_lines)} ADR events found.")
print_status_style("%i generic events found." % len(generic_events))
print_status_style("%i ADR events found." % len(adr_lines))
if warnings:
perform_adr_validations(iter(adr_lines))
+4 -1
View File
@@ -2,4 +2,7 @@
Docparser module
"""
from .pt_doc_parser import parse_document # pyright: ignore[reportUnusedImport, reportUnusedImport] # noqa: F401, I001
from .pt_doc_parser import parse_document
__all__ = [parse_document]
+17 -15
View File
@@ -1,9 +1,10 @@
from __future__ import annotations
from collections import namedtuple
from collections.abc import Generator, Iterator
from dataclasses import dataclass
from fractions import Fraction
from typing import Callable, NamedTuple
from typing import Callable
from ptulsconv.docparser import doc_entity
@@ -26,15 +27,12 @@ class TagCompiler:
items.
"""
class Intermediate(NamedTuple):
track_content: str
clip_content: str
track_tags: dict[str, str]
track_comment_tags: dict[str, str]
clip_tags: dict[str, str]
clip_tag_mode: TagPreModes
start: Fraction
finish: Fraction
Intermediate = namedtuple(
"Intermediate",
"track_content track_tags track_comment_tags "
"clip_content clip_tags clip_tag_mode start "
"finish",
)
session: doc_entity.SessionDescriptor
@@ -133,6 +131,7 @@ 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":
continue
@@ -154,6 +153,7 @@ class TagCompiler:
@staticmethod
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
@@ -177,13 +177,14 @@ class TagCompiler:
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:
time_spans.append((item.clip_tags, item.start, item.finish))
else:
yield item, tuple(time_spans)
yield item, list(time_spans)
@staticmethod
def _time_span_tags(at_time: Fraction, applicable_spans) -> dict:
@@ -198,6 +199,7 @@ class TagCompiler:
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:
@@ -224,13 +226,13 @@ class TagCompiler:
def apply_appends(
source: Iterator, should_append: Callable, preform_append: Callable
source: Iterator, should_append: Callable, do_append: Callable
) -> Generator:
"""
:param source:
:param should_append: Called with two variables a and b, your
function should return true if b should be
appended to a
function should return true if b should be
appended to a
:param do_append: Called with two variables a and b, your function
should return
:returns: A Generator
@@ -238,7 +240,7 @@ def apply_appends(
this_element = next(source)
for element in source:
if should_append(this_element, element):
this_element = preform_append(this_element, element)
this_element = do_append(this_element, element)
else:
yield this_element
this_element = element
@@ -1,4 +1,5 @@
from enum import Enum
from typing import Dict
from parsimonious import Grammar, NodeVisitor
@@ -36,7 +37,7 @@ def parse_tags(prompt: str) -> "TaggedStringResult":
class TaggedStringResult:
content: str
tag_dict: dict[str, str]
tag_dict: Dict[str, str]
mode: TagPreModes
def __init__(self, content, tag_dict, mode):
@@ -52,7 +53,7 @@ class TagListVisitor(NodeVisitor):
return TaggedStringResult(
content=next(iter(line_opt), None),
tag_dict=next(iter(tag_list_opt), {}),
tag_dict=next(iter(tag_list_opt), dict()),
mode=TagPreModes(next(iter(modifier_opt), "Normal")),
)
@@ -73,7 +74,7 @@ class TagListVisitor(NodeVisitor):
@staticmethod
def visit_tag_list(_, visited_children):
retdict = {}
retdict = dict()
for child in visited_children:
if child[0] is not None:
k, v = child[0]
+2 -3
View File
@@ -2,13 +2,12 @@
Methods for converting string reprentations of film footage.
"""
from __future__ import annotations
import re
from fractions import Fraction
from typing import Optional
def footage_to_seconds(footage: str) -> Fraction | None:
def footage_to_seconds(footage: str) -> Optional[Fraction]:
"""
Converts a string representation of a footage (35mm, 24fps)
into a :class:`Fraction`, this fraction being a some number of
+16 -21
View File
@@ -26,7 +26,7 @@ class ReportCanvas(canvas.Canvas):
def showPage(self):
self._saved_page_states.append(dict(self.__dict__))
self._startPage() # pyright: ignore[reportAttributeAccessIssue]
self._startPage()
def save(self):
"""add page info to each page (page x of y)"""
@@ -41,11 +41,9 @@ class ReportCanvas(canvas.Canvas):
self.saveState()
self.setFont("Helvetica", 10) # FIXME make this customizable
self.drawString(
0.5 * inch,
0.5 * inch,
f"Page {self._pageNumber} of {page_count}", # pyright: ignore[reportAttributeAccessIssue]
0.5 * inch, 0.5 * inch, "Page %d of %d" % (self._pageNumber, page_count)
)
right_edge = self._pagesize[0] - 0.5 * inch # pyright: ignore[reportAttributeAccessIssue]
right_edge = self._pagesize[0] - 0.5 * inch
self.drawRightString(
right_edge, 0.5 * inch, self._report_date.strftime("%m/%d/%Y %H:%M")
)
@@ -64,7 +62,7 @@ class ADRDocTemplate(BaseDocTemplate):
def make_doc_template(
page_size: tuple[float, float],
page_size,
filename,
document_title,
title: str,
@@ -73,22 +71,18 @@ def make_doc_template(
client: str,
document_subheader: str,
left_margin=0.5 * inch,
fonts: list[TTFont] | None = None,
fonts: list[TTFont] = [],
) -> ADRDocTemplate:
if fonts is None:
fonts = []
right_margin = top_margin = bottom_margin = 0.5 * inch
page_box: GRect = GRect(0.0, 0.0, page_size[0], page_size[1])
_, page_box = page_box.split_x(left_margin, direction="l") # pyright: ignore[reportAssignmentType]
_, page_box = page_box.split_x(right_margin, direction="r") # pyright: ignore[reportAssignmentType]
_, page_box = page_box.split_y(bottom_margin, direction="u") # pyright: ignore[reportAssignmentType]
_, page_box = page_box.split_y(top_margin, direction="d") # pyright: ignore[reportAssignmentType]
_, 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") # pyright: ignore[reportAssignmentType]
header_box, page_box = page_box.split_y(0.75 * inch, direction="d") # pyright: ignore[reportAssignmentType]
title_box, report_box = header_box.split_x(3.5 * inch, direction="r") # pyright: ignore[reportOptionalMemberAccess]
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")
def on_page_lambda(c, _):
draw_header_footer(
@@ -103,6 +97,7 @@ def make_doc_template(
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)
@@ -297,7 +292,7 @@ class GRect:
rem = self
for item in x_list:
s, rem = rem.split_x(item, direction) # pyright: ignore[reportOptionalMemberAccess]
s, rem = rem.split_x(item, direction)
ret_list.append(s)
return ret_list, rem
@@ -307,7 +302,7 @@ class GRect:
rem = self
for item in y_list:
s, rem = rem.split_y(item, direction) # pyright: ignore[reportOptionalMemberAccess]
s, rem = rem.split_y(item, direction)
ret_list.append(s)
return ret_list, rem
@@ -404,7 +399,7 @@ class GRect:
cp.rect(self.min_x, self.min_y, self.width, self.height)
a_canvas.clipPath(cp, stroke=0, fill=0)
_w, h = flowable.wrap(inset_rect.width, inset_rect.height)
w, h = flowable.wrap(inset_rect.width, inset_rect.height)
flowable.drawOn(a_canvas, inset_rect.x, inset_rect.max_y - h)
+38 -33
View File
@@ -1,8 +1,11 @@
from __future__ import annotations
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
@@ -11,13 +14,15 @@ from .__init__ import make_doc_template, time_format
def build_columns(
lines: list[ADRLine],
reel_list: list[str] | None,
lines: List[ADRLine],
reel_list: Optional[List[str]],
show_priorities=False,
include_omitted=False,
):
columns = []
reel_numbers = reel_list or sorted({x.reel for x in lines if x.reel is not None})
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
@@ -155,17 +160,17 @@ def build_columns(
):
columns.append(
{
"heading": f"P{n}",
"value_getter": lambda recs, N=n: len(
[r for r in recs if r.priority == N]
"heading": "P%i" % n,
"value_getter": lambda recs: len(
[r for r in recs if r.priority == n]
),
"value_getter2": (
lambda recs, N=n: time_format(
lambda recs: time_format(
sum(
[
r.time_budget_mins or 0.0
for r in recs
if r.priority == N
if r.priority == n
]
)
)
@@ -238,20 +243,22 @@ def build_columns(
return columns
def populate_columns(lines: list[ADRLine], columns, include_omitted, _page_size):
data = []
styles = []
columns_widths = []
def populate_columns(lines: List[ADRLine], columns, include_omitted, _page_size):
data = list()
styles = list()
columns_widths = list()
sorted_character_numbers: list[str | None] = sorted(
{x.character_id for x in lines}, key=lambda x: str(x)
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([x["heading"] for x in columns])
data.append(list(map(lambda x: x["heading"], columns)))
if not include_omitted:
lines = [x for x in lines if not x.omitted]
@@ -259,8 +266,8 @@ def populate_columns(lines: list[ADRLine], columns, include_omitted, _page_size)
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 = []
row_data2 = []
row_data = list()
row_data2 = list()
for col in columns:
row1_index = len(data)
@@ -284,8 +291,8 @@ def populate_columns(lines: list[ADRLine], columns, include_omitted, _page_size)
data.append(row_data)
data.append(row_data2)
summary_row1 = []
summary_row2 = []
summary_row1 = list()
summary_row2 = list()
row1_index = len(data)
for col in columns:
@@ -309,15 +316,12 @@ def populate_columns(lines: list[ADRLine], columns, include_omitted, _page_size)
def output_report(
lines: list[ADRLine],
reel_list: list[str],
page_size: tuple[float, float] | None = None,
lines: List[ADRLine],
reel_list: List[str],
include_omitted=False,
page_size=portrait(letter),
font_name="Helvetica",
):
if page_size is None:
page_size = portrait(letter)
columns = build_columns(lines, include_omitted=include_omitted, reel_list=reel_list)
data, style, columns_widths = populate_columns(
lines, columns, include_omitted, page_size
@@ -330,17 +334,16 @@ def output_report(
# pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
title = f"{lines[0].title} Line Count"
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 or "",
client=lines[0].client or "",
supervisor=lines[0].supervisor or "",
document_subheader=lines[0].spot,
client=lines[0].client,
supervisor=lines[0].supervisor,
document_header="Line Count",
)
@@ -361,6 +364,8 @@ def output_report(
omitted_count = len([x for x in lines if x.omitted])
if not include_omitted and omitted_count > 0:
story.append(Paragraph(f"* {omitted_count} Omitted lines are excluded.", style))
story.append(
Paragraph("* %i Omitted lines are excluded." % omitted_count, style)
)
doc.build(story)
+41 -32
View File
@@ -1,4 +1,5 @@
from __future__ import annotations
from typing import List
from reportlab.lib.pagesizes import letter, portrait
from reportlab.lib.styles import getSampleStyleSheet
@@ -12,7 +13,7 @@ from .__init__ import make_doc_template, time_format
def build_aux_data_field(line: ADRLine):
entries = []
entries = list()
if line.reason is not None:
entries.append("Reason: " + line.reason)
if line.note is not None:
@@ -26,30 +27,40 @@ def build_aux_data_field(line: ADRLine):
tag_field = ""
if line.effort:
bg_color = "red"
tag_field += (
f"<font backColor={bg_color} textColor={fg_color} fontSize=11>EFF</font> "
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " % (
bg_color,
fg_color,
"EFF",
)
elif line.tv:
bg_color = "blue"
tag_field += (
f"<font backColor={bg_color} textColor={fg_color} fontSize=11>TV</font> "
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " % (
bg_color,
fg_color,
"TV",
)
elif line.adlib:
bg_color = "purple"
tag_field += (
f"<font backColor={bg_color} textColor={fg_color} fontSize=11>ADLIB</font> "
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " % (
bg_color,
fg_color,
"ADLIB",
)
elif line.optional:
bg_color = "green"
tag_field += f"font backColor={bg_color} textColor={fg_color} fontSize=11>OPTIONAL</font>"
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 = []
def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat, font_name="Helvetica"):
story = list()
this_scene = None
scene_style = getSampleStyleSheet()["Normal"]
@@ -67,14 +78,15 @@ def build_story(lines: list[ADRLine], tc_rate: TimecodeFormat, font_name="Helvet
("BOTTOMPADDING", (0, 0), (-1, -1), 24.0),
]
cue_number_field = (
f"{line.cue_number}<br /><font fontSize=7>{line.character_name}</font>"
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: " + str(line.priority)
time_data = time_data + "<br />" + "P: " + line.priority
aux_data_field = build_aux_data_field(line)
@@ -84,7 +96,7 @@ def build_story(lines: list[ADRLine], tc_rate: TimecodeFormat, font_name="Helvet
[
Paragraph(cue_number_field, line_style),
Paragraph(tc_data, line_style),
Paragraph(line.prompt or "[No Prompt]", line_style),
Paragraph(line.prompt, line_style),
Paragraph(time_data, line_style),
Paragraph(aux_data_field, line_style),
]
@@ -123,11 +135,11 @@ def build_tc_data(line: ADRLine, tc_format: TimecodeFormat):
third_line = []
if line.reel is not None:
if line.reel[0:1] == "R":
third_line.append(f"{line.reel}")
third_line.append("%s" % line.reel)
else:
third_line.append(f"Reel {line.reel}")
third_line.append("Reel %s" % line.reel)
if line.version is not None:
third_line.append(f"({line.version})")
third_line.append("(%s)" % line.version)
if len(third_line) > 0:
tc_data = tc_data + "<br/>" + " ".join(third_line)
return tc_data
@@ -135,17 +147,17 @@ def build_tc_data(line: ADRLine, tc_format: TimecodeFormat):
def generate_report(
page_size,
lines: list[ADRLine],
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 = f"{lines[0].title} ADR Report ({lines[0].character_name})"
document_header = f"{lines[0].character_name} ADR Report"
title = "%s ADR Report (%s)" % (lines[0].title, lines[0].character_name)
document_header = "%s ADR Report" % lines[0].character_name
else:
title = f"{lines[0].title} ADR Report"
title = "%s ADR Report" % lines[0].title
document_header = "ADR Report"
if not include_omitted:
@@ -160,9 +172,9 @@ def generate_report(
document_title=title,
document_header=document_header,
title=lines[0].title,
supervisor=lines[0].supervisor or "",
client=lines[0].client or "",
document_subheader=lines[0].spot or "",
supervisor=lines[0].supervisor,
client=lines[0].client,
document_subheader=lines[0].spot,
left_margin=0.75 * inch,
)
story = build_story(lines, tc_rate)
@@ -170,16 +182,13 @@ def generate_report(
def output_report(
lines: list[ADRLine],
lines: List[ADRLine],
tc_display_format: TimecodeFormat,
page_size: tuple[float, float] | None = None,
by_character: bool = False,
page_size=portrait(letter),
by_character=False,
):
if page_size is None:
page_size = portrait(letter)
if by_character:
character_numbers = {r.character_id for r in lines}
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:
+14 -10
View File
@@ -24,7 +24,7 @@ def draw_header_block(canvas, rect, record: ADRLine):
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 = f"{record.character_id} / {record.character_name}"
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)
@@ -61,9 +61,9 @@ def draw_cue_number_block(canvas, rect, record: ADRLine):
"omitted": "OMIT",
}
tag_field = ""
for key, value in tags.items():
for key in tags:
if getattr(record, key):
tag_field = tag_field + value + " "
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"
@@ -221,7 +221,7 @@ def draw_aux_block(canvas, rect, recording_time_sec_this_line, recording_time_se
rect.draw_border(canvas, "min_x")
content_rect = rect.inset_xy(10.0, 10.0)
lines, _ = content_rect.divide_y(
lines, last_line = content_rect.divide_y(
[12.0, 12.0, 24.0, 24.0, 24.0, 24.0], direction="d"
)
@@ -255,7 +255,7 @@ 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 = [f"Line {line_no} of {total_lines}"]
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
@@ -266,8 +266,11 @@ def create_report_for_character(
records, report_date, tc_display_format: TimecodeFormat
):
outfile = f"{records[0].title}_{records[0].character_id}_{records[0].character_name}_Log.pdf"
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!"
@@ -282,7 +285,7 @@ def create_report_for_character(
)
)
cue_header_block, _title_header_block = header_row.split_x(4.0 * inch)
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]
)
@@ -294,7 +297,8 @@ def create_report_for_character(
)
c.setTitle(
f"{records[0].title} {records[0].character_name} ({records[0].character_id}) Supervisor's Log"
"%s %s (%s) Supervisor's Log"
% (records[0].title, records[0].character_name, records[0].character_id)
)
c.setAuthor(records[0].supervisor)
@@ -335,7 +339,7 @@ def create_report_for_character(
def output_report(lines, tc_display_format: TimecodeFormat):
report_date = datetime.datetime.now()
events = sorted(lines, key=lambda x: x.start)
character_numbers = {x.character_id for x in lines}
character_numbers = set([x.character_id for x in lines])
for n in character_numbers:
create_report_for_character(
+8 -6
View File
@@ -1,3 +1,5 @@
from typing import List
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet
@@ -12,9 +14,9 @@ from .__init__ import make_doc_template
def output_report(
lines: list[ADRLine], tc_display_format: TimecodeFormat, font_name="Helvetica"
lines: List[ADRLine], tc_display_format: TimecodeFormat, font_name="Helvetica"
):
character_numbers = {n.character_id for n in lines}
character_numbers = set([n.character_id for n in lines])
# pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
for n in character_numbers:
@@ -25,8 +27,8 @@ def output_report(
char_lines = sorted(char_lines, key=lambda line: line.start)
title = f"{char_lines[0].title} ({character_name}) {n} ADR Script"
filename = f"{char_lines[0].title}_{n}_{character_name}_ADR Script.pdf"
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,
@@ -62,7 +64,7 @@ def output_report(
finish_tc = tc_display_format.seconds_to_smpte(line.finish)
data_block = [
[
Paragraph(line.cue_number or "[No QN]", number_style),
Paragraph(line.cue_number, number_style),
Paragraph(start_tc + " - " + finish_tc, number_style),
]
]
@@ -78,7 +80,7 @@ def output_report(
colWidths=[1.5 * inch, 6.0 * inch],
style=[("LEFTPADDING", (0, 0), (-1, -1), 0.0)],
),
Paragraph(line.prompt or "[No Prompt]", prompt_style),
Paragraph(line.prompt, prompt_style),
Spacer(1.0, inch * 1.5),
]
)
+3 -1
View File
@@ -43,7 +43,9 @@ def print_advisory_tagging_error(
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')
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')
+10 -18
View File
@@ -1,13 +1,11 @@
from __future__ import annotations
import datetime
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 typing import List
from xml.etree.ElementTree import TreeBuilder, tostring
import ptulsconv
@@ -17,16 +15,13 @@ from ptulsconv.docparser.adr_entity import ADRLine
def avid_marker_list(
lines: list[ADRLine],
report_date: datetime | None = None,
lines: List[ADRLine],
report_date=datetime.datetime.now(),
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"})
@@ -62,8 +57,6 @@ def avid_marker_list(
doc.start("List", {"id": "OMFI:ATTR:AttrRefs"})
assert report_date
insert_elem(
"1",
"OMFI:ATTB:IntAttribute",
@@ -82,7 +75,7 @@ def avid_marker_list(
line.supervisor or "",
)
marker_name = f"{line.cue_number}: {line.prompt}"
marker_name = "%s: %s" % (line.cue_number, line.prompt)
insert_elem(
"2", "OMFI:ATTB:StringAttribute", "string", "_ATN_CRM_COM", marker_name
)
@@ -119,8 +112,7 @@ def dump_fmpxml(data, input_file_name, output, adr_field_map):
doc.data("0")
doc.end("ERRORCODE")
version = module_version("ptulsconv")
doc.start("PRODUCT", {"NAME": ptulsconv.__name__, "VERSION": f"{version}"})
doc.start("PRODUCT", {"NAME": ptulsconv.__name__, "VERSION": ptulsconv.__version__})
doc.end("PRODUCT")
doc.start(
@@ -155,7 +147,7 @@ def dump_fmpxml(data, input_file_name, output, adr_field_map):
doc.start("COL", {})
doc.start("DATA", {})
for key_attempt in field[0]:
if key_attempt in event:
if key_attempt in event.keys():
doc.data(str(event[key_attempt]))
break
doc.end("DATA")
@@ -178,7 +170,7 @@ def xform_options():
def dump_xform_options(output=sys.stdout):
print("# Available transforms:", file=output)
print(f"# Transform dir: {xslt_path}", 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)
@@ -196,14 +188,14 @@ def fmp_transformed_dump(data, input_file, xsl_name, output, adr_field_map):
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("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(f"Using xsl: {xsl_path}")
print_status_style("Using xsl: %s" % xsl_path)
subprocess.run(
["xsltproc", xsl_path, "-"],
input=str_data,