75 Commits

Author SHA1 Message Date
Jamie Hardt
4a0d19ade1 Added 3.13 to classifiers. 2025-05-25 07:58:55 -07:00
Jamie Hardt
df6c783c51 Added 3.13 to test matrix 2025-05-25 07:57:42 -07:00
Jamie Hardt
f0b232b2b6 autopep 2025-05-25 07:54:50 -07:00
Jamie Hardt
519c6403ba Nudged version 2025-05-25 07:52:11 -07:00
Jamie Hardt
d29c36eafa Fixed some bugs I introduced, fixed entrypoint 2025-05-25 07:50:37 -07:00
Jamie Hardt
2095a1fb75 Nudged version 2025-05-25 07:24:07 -07:00
Jamie Hardt
70defcc46c fixed typo in copyright line 2025-05-25 07:23:25 -07:00
Jamie Hardt
d156b6df89 Added notes 2025-05-25 07:21:50 -07:00
Jamie Hardt
3ba9d7933e Merge branch 'master' of https://github.com/iluvcapra/ptulsconv 2025-05-25 07:16:45 -07:00
Jamie Hardt
b0c40ee0b6 Merged 2025-05-25 07:16:35 -07:00
Jamie Hardt
921b0f07af Update pyproject.toml
Fixing sphinx dependencies
2025-05-25 07:13:11 -07:00
Jamie Hardt
57764bc859 Update conf.py 2025-05-25 07:05:24 -07:00
Jamie Hardt
779c93282c Update conf.py
Updated copyright message
2025-05-25 07:04:17 -07:00
Jamie Hardt
9684be6c7e Update __init__.py 2025-05-25 07:03:28 -07:00
Jamie Hardt
484a70fc8e Update __init__.py 2025-05-25 07:01:47 -07:00
Jamie Hardt
5aa005c317 Update conf.py 2025-05-25 07:00:44 -07:00
Jamie Hardt
454adea3d1 Merge pull request #13 from iluvcapra/maint-poetry
Upgrade build tool to Poetry
2025-05-24 22:25:53 -07:00
Jamie Hardt
1e6546dab5 Tweak file for flake 2025-05-24 22:24:45 -07:00
Jamie Hardt
8b262d3bfb Rearranged pyproject, brought in metadata 2025-05-24 22:22:04 -07:00
Jamie Hardt
630e7960dc Making changes for peotry 2025-05-24 22:20:15 -07:00
Jamie Hardt
aa7b418121 Update __init__.py
Nudging version to 2.2.1
2025-05-24 21:58:50 -07:00
Jamie Hardt
a519a525b2 Update pythonpublish.yml
Updating python publish action to the latest version
2025-05-24 21:54:42 -07:00
Jamie Hardt
1412efe509 autopep 2025-05-18 13:39:06 -07:00
Jamie Hardt
12a6c05467 autopep 2025-05-18 13:37:46 -07:00
Jamie Hardt
cf87986014 autopep'd test 2025-05-18 13:35:12 -07:00
Jamie Hardt
67533879f8 Rewrote parsing to handle old & new-style markers 2025-05-18 13:33:51 -07:00
Jamie Hardt
f847b88aa3 Nudged version and copyright date 2025-05-17 12:06:56 -07:00
Jamie Hardt
c3a600c5d7 Integrated track marker test case and fixed parser 2025-05-17 12:05:27 -07:00
Jamie Hardt
914783a809 Updated documentation 2025-05-17 11:26:07 -07:00
Jamie Hardt
c638c673e8 Adding track marker export case 2025-05-17 11:23:54 -07:00
Jamie Hardt
15fe6667af Fixed up unit test 2025-05-17 11:23:02 -07:00
Jamie Hardt
d4e23b59eb Adding support for track markers
(Always ignore for now)
2025-05-17 11:19:22 -07:00
Jamie Hardt
a602b09551 flake8 2025-05-17 10:47:21 -07:00
Jamie Hardt
448d93d717 Fix for flake 2025-05-17 10:45:40 -07:00
Jamie Hardt
59e7d40d97 Merge branch 'master' of https://github.com/iluvcapra/ptulsconv 2025-05-11 22:19:26 -07:00
Jamie Hardt
eaa5fe824f Fixed parser logic to handle new-style marker tracks 2025-05-11 22:17:42 -07:00
Jamie Hardt
8ebfd32e02 Update __init__.py
Nudge version
2024-07-10 21:16:45 -07:00
Jamie Hardt
83a9adb48a Merge remote-tracking branch 'refs/remotes/origin/master' 2023-11-15 23:09:27 -08:00
Jamie Hardt
013ebcbe75 movie options 2023-11-15 23:09:12 -08:00
Jamie Hardt
c87695e5fe Merge pull request #12 from iluvcapra/maint-py312
Add Python 3.12 support
2023-11-08 10:33:13 -08:00
Jamie Hardt
4a8983cbbb Update python-package.yml
Added 3.12 to test matrix
2023-11-08 10:27:56 -08:00
Jamie Hardt
9123cbd0b5 Update pyproject.toml
Added 3.12 classifier
2023-11-08 10:27:04 -08:00
Jamie Hardt
4224d106b0 Fixed compyright notice 2023-11-04 11:50:03 -07:00
Jamie Hardt
ac22fab97f Some style fixes (all E231) 2023-11-04 11:36:34 -07:00
Jamie Hardt
64ca2c6c5c Silenced some more errors 2023-11-04 11:23:40 -07:00
Jamie Hardt
c3af30dc6a Renamed my JSONEncoder something useful 2023-11-04 11:21:59 -07:00
Jamie Hardt
c30f675cec Cleared up a type warning 2023-11-04 11:17:48 -07:00
Jamie Hardt
204af7d9cb A bunch of typo cleanups and styling. 2023-11-04 11:13:49 -07:00
Jamie Hardt
10fc211e80 Some typos 2023-11-04 10:56:44 -07:00
Jamie Hardt
d56c7df376 Updated documentation to reflect current usage
No longer have to output a text export.
Some formatting changes.
2023-11-04 10:49:56 -07:00
Jamie Hardt
7b38449a5f Fixed formatting of a list. 2023-11-04 10:43:21 -07:00
Jamie Hardt
17b87b6e69 Update __init__.py
Nudged version
2023-07-27 23:23:39 -07:00
Jamie Hardt
a636791539 Autopep 2023-07-27 23:17:23 -07:00
Jamie Hardt
dfde3c4493 Fixed errors with track_index field
In tests
2023-07-27 23:15:49 -07:00
Jamie Hardt
81909c8a51 Added track index to TrackDescriptor
to indicate a track's import order.
2023-07-27 22:58:06 -07:00
Jamie Hardt
e2b9a20870 Added some documentation 2023-07-27 22:10:29 -07:00
Jamie Hardt
006cec05e5 Merge pull request #10 from iluvcapra/bug-flake8
Flake8 code cleanups and a bug fix
2023-07-22 13:01:15 -07:00
Jamie Hardt
a95f0b5cca Nudged version number 2023-07-22 12:58:32 -07:00
Jamie Hardt
70a5206d73 Fixed dumb typo that made ptsl break 2023-07-21 22:21:48 -07:00
Jamie Hardt
128eed002d Update README.md 2023-07-21 14:54:54 -07:00
Jamie Hardt
f8a0d70942 Update README.md
Dumb typo in "last commit" badge
2023-07-21 14:54:23 -07:00
Jamie Hardt
5f29e95ba9 Merge pull request #8 from iluvcapra/bug-flake8
Add Flake8 to build tests, clean up code style
2023-07-21 14:26:53 -07:00
Jamie Hardt
82f07b13a6 Do not warn on unsued imports in __init__ 2023-07-21 14:25:17 -07:00
Jamie Hardt
fbcbba1098 flake8 2023-07-21 14:20:35 -07:00
Jamie Hardt
622f04963f Update python-package.yml
Added flake8 to the build
2023-07-21 14:04:35 -07:00
Jamie Hardt
5b36dcb5aa flake8 2023-07-21 14:03:05 -07:00
Jamie Hardt
fd02d962d0 flake8 2023-07-21 13:45:47 -07:00
Jamie Hardt
2021159666 flake8 fixes 2023-07-21 13:38:24 -07:00
Jamie Hardt
f825b92586 Flake8 cleanups 2023-07-21 13:21:01 -07:00
Jamie Hardt
4318946596 Merge pull request #7 from iluvcapra/require-py-3.8
Eliminate Python 3.7 Support
2023-07-21 12:57:46 -07:00
Jamie Hardt
2a98954885 Update __init__.py 2023-07-21 12:56:13 -07:00
Jamie Hardt
79d8cc5b69 Update python-package.yml 2023-07-21 12:53:43 -07:00
Jamie Hardt
5785dc3364 Update pyproject.toml
Requires Python 3.8
2023-07-21 12:51:12 -07:00
Jamie Hardt
4e64edcd85 Updated tests 2023-07-21 12:44:42 -07:00
Jamie Hardt
58277367c5 Implemeneted direct reading session data with PTSL 2023-07-21 12:33:59 -07:00
41 changed files with 947 additions and 534 deletions

4
.flake8 Normal file
View File

@@ -0,0 +1,4 @@
[flake8]
per-file-ignores =
ptulsconv/__init__.py: F401
ptulsconv/docparser/__init__.py: F401

View File

@@ -16,7 +16,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v2.5.0 - uses: actions/checkout@v2.5.0
@@ -38,3 +38,4 @@ jobs:
- name: Test with pytest - name: Test with pytest
run: | run: |
pytest pytest
flake8 ptulsconv

View File

@@ -26,7 +26,7 @@ jobs:
- name: Build package - name: Build package
run: python -m build run: python -m build
- name: pypi-publish - name: pypi-publish
uses: pypa/gh-action-pypi-publish@v1.8.6 uses: pypa/gh-action-pypi-publish@v1.12.4
# - name: Report to Mastodon # - name: Report to Mastodon
# uses: cbrgm/mastodon-github-action@v1.0.1 # uses: cbrgm/mastodon-github-action@v1.0.1
# with: # with:

View File

@@ -2,7 +2,7 @@
![](https://img.shields.io/github/license/iluvcapra/ptulsconv.svg) ![](https://img.shields.io/github/license/iluvcapra/ptulsconv.svg)
![](https://img.shields.io/pypi/pyversions/ptulsconv.svg) ![](https://img.shields.io/pypi/pyversions/ptulsconv.svg)
[![](https://img.shields.io/pypi/v/ptulsconv.svg)][pypi] [![](https://img.shields.io/pypi/v/ptulsconv.svg)][pypi]
![GitHub last commit](https://img.shields.io/github/last-commit/iluvcapra/pycmx) ![GitHub last commit](https://img.shields.io/github/last-commit/iluvcapra/ptulsconv)
[![Lint and Test](https://github.com/iluvcapra/ptulsconv/actions/workflows/python-package.yml/badge.svg)](https://github.com/iluvcapra/ptulsconv/actions/workflows/python-package.yml) [![Lint and Test](https://github.com/iluvcapra/ptulsconv/actions/workflows/python-package.yml/badge.svg)](https://github.com/iluvcapra/ptulsconv/actions/workflows/python-package.yml)
[pypi]: https://pypi.org/project/ptulsconv/ [pypi]: https://pypi.org/project/ptulsconv/
@@ -10,7 +10,7 @@
# ptulsconv # ptulsconv
Read Pro Tools text exports and generate PDF reports, JSON output. Parse Pro Tools text exports and generate PDF reports, JSON output.
## Quick Start ## Quick Start

View File

@@ -3,6 +3,7 @@
# For the full list of built-in configuration values, see the documentation: # For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html # https://www.sphinx-doc.org/en/master/usage/configuration.html
import importlib
import sys import sys
import os import os
@@ -15,9 +16,9 @@ import ptulsconv
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'ptulsconv' project = 'ptulsconv'
# copyright = ptulsconv.__copyright__ copyright = '2019-2025 Jamie Hardt. All rights reserved'
# author = ptulsconv.__author__ version = "Version 2"
release = ptulsconv.__version__ release = importlib.metadata.version("ptulsconv")
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@@ -6,7 +6,12 @@ Usage Form
Invocations of ptulsconv take the following form: Invocations of ptulsconv take the following form:
ptulsconv [options] IN_FILE ptulsconv [options] [IN_FILE]
`IN_FILE` is a Pro Tools text export in UTF-8 encoding. If `IN_FILE` is
missing, `ptulsconv` will attempt to connect to Pro Tools and read cue data
from the selected tracks of the currently-open session.
Flags Flags

View File

@@ -24,19 +24,21 @@ Step 2: Add More Information to Your Spots
Clips, tracks and markers in your session can contain additional information Clips, tracks and markers in your session can contain additional information
to make your ADR reports more complete and useful. You add this information to make your ADR reports more complete and useful. You add this information
with *tagging*. with :ref:`tagging<tags>`.
* Every ADR clip must have a unique cue number. After the name of each clip, * **Every ADR clip must have a unique cue number.** After the name of each
add the letters "$QN=" and then a unique number (any combination of letters clip, add the letters ``$QN=`` and then a unique number (any combination of
or numbers that don't contain a space). You can type these yourself or add letters or numbers that don't contain a space). You can type these yourself
them with batch-renaming when you're done spotting. or add them with batch-renaming when you're done spotting.
* ADR spots should usually have a reason indicated, so you can remember exactly * ADR spots should usually have a reason indicated, so you can remember exactly
why you're replacing a particular line. Do this by adding the the text "{R=" why you're replacing a particular line. Do this by adding the the text
to your clip names after the prompt and then some short text describing the ``{R=`` to your clip names after the prompt and then some short text
reason, and then a closing "}". You can type anything, including spaces. describing the reason, and then a closing ``}``. You can type anything,
* If a line is a TV cover line, you can add the text "[TV]" to the end. including spaces.
* If, for example, a line is a TV cover line, you can add the text ``[TV]`` to
the end.
So for example, some ADR spot's clip name might look like: So for example, some ADR spot's clip name might look like::
Get to the ladder! {R=Noise} $QN=J1001 Get to the ladder! {R=Noise} $QN=J1001
"Forget your feelings! {R=TV Cover} $QN=J1002 [TV] "Forget your feelings! {R=TV Cover} $QN=J1002 [TV]
@@ -45,32 +47,26 @@ These tags can appear in any order.
* You can add the name of an actor to a character's track, so this information * You can add the name of an actor to a character's track, so this information
will appear on your reports. In the track name, or in the track comments, will appear on your reports. In the track name, or in the track comments,
type "{Actor=xxx}" replacing the xxx with the actor's name. type ``{Actor=xxx}`` replacing the xxx with the actor's name.
* Characters need to have a number (perhaps from the cast list) to express how * Characters need to have a number (perhaps from the cast list) to express how
they should be collated. Add "$CN=xxx" with a unique number to each track (or they should be collated. Add ``$CN=xxx`` with
the track's comments.) a unique number to each track (or the track's comments.)
* Set the scene for each line with markers. Create a marker at the beginning of * Set the scene for each line with markers. Create a marker at the beginning of
a scene and make it's name "{Sc=xxx}", replacing the xxx with the scene a scene and make it's name ``{Sc=xxx}``, replacing the xxx with the scene
number and name. number and name.
Step 3: Export Tracks from Pro Tools as a Text File Step 3: Run `ptulsconv`
--------------------------------------------------- ------------------------
Export the file as a UTF-8 and be sure to include clips and markers. Export In Pro Tools, select the tracks that contain your spot clips.
using the Timecode time format.
Do not export crossfades. Then, in your Terminal, run the following command::
ptulsconv
Step 4: Run `ptulsconv` on the Text Export `ptulsconv` will connect to Pro Tools and read all of the clips on the selected
------------------------------------------ track. It will then create a folder named "Title_CURRENT_DATE", and within that
In your Terminal, run the following command:
ptulsconv path/to/your/TEXT_EXPORT.txt
`ptulsconv` will create a folder named "Title_CURRENT_DATE", and within that
folder it will create several PDFs and folders: folder it will create several PDFs and folders:
- "TITLE ADR Report" 📄 a PDF tabular report of every ADR line you've spotted. - "TITLE ADR Report" 📄 a PDF tabular report of every ADR line you've spotted.

View File

@@ -4,8 +4,8 @@ Tagging
======= =======
Tags are used to add additional data to a clip in an organized way. The Tags are used to add additional data to a clip in an organized way. The
tagging system in `ptulsconv` allows is flexible and can be used to add tagging system in `ptulsconv` is flexible and can be used to add any kind of
any kind of extra data to a clip. extra data to a clip.
Fields in Clip Names Fields in Clip Names
-------------------- --------------------
@@ -14,7 +14,7 @@ Track names, track comments, and clip names can also contain meta-tags, or
"fields," to add additional columns to the output. Thus, if a clip has the "fields," to add additional columns to the output. Thus, if a clip has the
name::: name:::
`Fireworks explosion {note=Replace for final} $V=1 [FX] [DESIGN]` Fireworks explosion {note=Replace for final} $V=1 [FX] [DESIGN]
The row output for this clip will contain columns for the values: The row output for this clip will contain columns for the values:
@@ -27,20 +27,24 @@ The row output for this clip will contain columns for the values:
These fields can be defined in the clip name in three ways: These fields can be defined in the clip name in three ways:
* `$NAME=VALUE` creates a field named `NAME` with a one-word value `VALUE`.
* `{NAME=VALUE}` creates a field named `NAME` with the value `VALUE`. `VALUE` * ``$NAME=VALUE`` creates a field named ``NAME`` with a one-word value
in this case may contain spaces or any chartacter up to the closing bracket. ``VALUE``.
* `[NAME]` creates a field named `NAME` with a value `NAME`. This can be used * ``{NAME=VALUE}`` creates a field named ``NAME`` with the value ``VALUE``.
to create a boolean-valued field; in the output, clips with the field ``VALUE`` in this case may contain spaces or any chartacter up to the
will have it, and clips without will have the column with an empty value. closing bracket.
* ``[NAME]`` creates a field named ``NAME`` with a value ``NAME``. This can
be used to create a boolean-valued field; in the output, clips with the
field will have it, and clips without will have the column with an empty
value.
For example, if three clips are named::: For example, if three clips are named:::
`"Squad fifty-one, what is your status?" [FUTZ] {Ch=Dispatcher} [ADR]` "Squad fifty-one, what is your status?" [FUTZ] {Ch=Dispatcher} [ADR]
`"We are ten-eight at Rampart Hospital." {Ch=Gage} [ADR]` "We are ten-eight at Rampart Hospital." {Ch=Gage} [ADR]
`(1M) FC callouts rescuing trapped survivors. {Ch=Group} $QN=1001 [GROUP]` (1M) FC callouts rescuing trapped survivors. {Ch=Group} $QN=1001 [GROUP]
The output will contain the range: The output will contain the range:
@@ -63,7 +67,7 @@ Fields in Track Names and Markers
--------------------------------- ---------------------------------
Fields set in track names, and in track comments, will be applied to *each* Fields set in track names, and in track comments, will be applied to *each*
clip on that track. If a track comment contains the text `{Dept=Foley}` for clip on that track. If a track comment contains the text ``{Dept=Foley}`` for
example, every clip on that track will have a "Foley" value in a "Dept" column. example, every clip on that track will have a "Foley" value in a "Dept" column.
Likewise, fields set on the session name will apply to all clips in the session. Likewise, fields set on the session name will apply to all clips in the session.
@@ -72,7 +76,10 @@ Fields set in markers, and in marker comments, will be applied to all clips
whose finish is *after* that marker. Fields in markers are applied cumulatively whose finish is *after* that marker. Fields in markers are applied cumulatively
from breakfast to dinner in the session. The latest marker applying to a clip has from breakfast to dinner in the session. The latest marker applying to a clip has
precedence, so if one marker comes after the other, but both define a field, the precedence, so if one marker comes after the other, but both define a field, the
value in the later marker value in the later marker.
All markers on all rulers will be scanned for tags. All markers on tracks will
be ignored.
An important note here is that, always, fields set on the clip name have the An important note here is that, always, fields set on the clip name have the
highest precedence. If a field is set in a clip name, the same field set on the highest precedence. If a field is set in a clip name, the same field set on the
@@ -84,17 +91,17 @@ track, the value set on the clip will prevail.
Apply Fields to a Time Range of Clips Apply Fields to a Time Range of Clips
------------------------------------- -------------------------------------
A clip name beginning with "@" will not be included in the output, but its A clip name beginning with ``@`` will not be included in the output, but its
fields will be applied to clips within its time range on lower tracks. fields will be applied to clips within its time range on lower tracks.
If track 1 has a clip named `@ {Sc=1- The House}`, any clips beginning within If track 1 has a clip named ``@ {Sc=1- The House}``, any clips beginning within
that range on lower tracks will have a field `Sc` with that value. that range on lower tracks will have a field ``Sc`` with that value.
Combining Clips with Long Names or Many Tags Combining Clips with Long Names or Many Tags
-------------------------------------------- --------------------------------------------
A clip name beginning with `&` will have its parsed clip name appended to the A clip name beginning with ``&`` will have its parsed clip name appended to the
preceding cue, and the fields of following cues will be applied, earlier clips preceding cue, and the fields of following cues will be applied, earlier clips
having precedence. The clips need not be touching, and the clips will be having precedence. The clips need not be touching, and the clips will be
combined into a single row of the output. The start time of the first clip will combined into a single row of the output. The start time of the first clip will
@@ -108,23 +115,24 @@ Setting Document Options
.. note:: .. note::
Document options are not yet implemented. Document options are not yet implemented.
A clip beginning with `!` sends a command to `ptulsconv`. These commands can ..
appear anywhere in the document and apply to the entire document. Commands are A clip beginning with ``!`` sends a command to `ptulsconv`. These commands can
a list of words appear anywhere in the document and apply to the entire document. Commands are
a list of words
The following commands are available: The following commands are available:
page $SIZE=`(letter|legal|a4)` page $SIZE=`(letter|legal|a4)`
Sets the PDF page size for the output. Sets the PDF page size for the output.
font {NAME=`name`} {PATH=`path`} font {NAME=`name`} {PATH=`path`}
Sets the primary font for the output. Sets the primary font for the output.
sub `replacement text` {FOR=`text_to_replace`} {IN=`tag`} sub `replacement text` {FOR=`text_to_replace`} {IN=`tag`}
Declares a substitution. Whereever text_to_replace is encountered in the Declares a substitution. Whereever text_to_replace is encountered in the
document it will be replaced with "replacement text". document it will be replaced with "replacement text".
If `tag` is set, this substitution will only be applied to the values of If `tag` is set, this substitution will only be applied to the values of
that tag. that tag.

Binary file not shown.

View File

@@ -1,8 +1,3 @@
""" """
Parse and convert Pro Tools text exports Parse and convert Pro Tools text exports
""" """
__version__ = '1.0.7'
__author__ = 'Jamie Hardt'
__license__ = 'MIT'
__copyright__ = "%s %s (c) 2023 %s. All rights reserved." % (__name__, __version__, __author__)

View File

@@ -2,9 +2,11 @@ from optparse import OptionParser, OptionGroup
import datetime import datetime
import sys import sys
from ptulsconv import __name__, __version__, __author__, __copyright__ import ptulsconv
from ptulsconv.commands import convert from ptulsconv.commands import convert
from ptulsconv.reporting import print_status_style, print_banner_style, print_section_header_style, print_fatal_error from ptulsconv.reporting import print_status_style, \
print_banner_style, print_section_header_style, \
print_fatal_error
def dump_field_map(output=sys.stdout): def dump_field_map(output=sys.stdout):
@@ -19,18 +21,18 @@ def dump_formats():
print_section_header_style("`raw` format:") print_section_header_style("`raw` format:")
sys.stderr.write("A JSON document of the parsed Pro Tools export.\n") sys.stderr.write("A JSON document of the parsed Pro Tools export.\n")
print_section_header_style("`tagged` Format:") print_section_header_style("`tagged` Format:")
sys.stderr.write("A JSON document containing one record for each clip, with\n" sys.stderr.write(
"all tags parsed and all tagging rules applied. \n") "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:") print_section_header_style("`doc` format:")
sys.stderr.write("Creates a directory with folders for different types\n" sys.stderr.write("Creates a directory with folders for different types\n"
"of ADR reports.\n\n") "of ADR reports.\n\n")
def main(): def main():
"""Entry point for the command-line invocation""" """Entry point for the command-line invocation"""
parser = OptionParser() parser = OptionParser()
parser.usage = "ptulsconv [options] TEXT_EXPORT.txt" parser.usage = "ptulsconv [options] [TEXT_EXPORT.txt]"
parser.add_option('-f', '--format', parser.add_option('-f', '--format',
dest='output_format', dest='output_format',
@@ -39,44 +41,54 @@ def main():
default='doc', default='doc',
help='Set output format, `raw`, `tagged`, `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", warn_options = OptionGroup(title="Warning and Validation Options",
parser=parser) parser=parser)
warn_options.add_option('-W', action='store_false', warn_options.add_option('-W', action='store_false',
dest='warnings', dest='warnings',
default=True, default=True,
help='Suppress warnings for common errors (missing code numbers etc.)') help='Suppress warnings for common '
'errors (missing code numbers etc.)')
parser.add_option_group(warn_options) parser.add_option_group(warn_options)
informational_options = OptionGroup(title="Informational Options", informational_options = OptionGroup(title="Informational Options",
parser=parser, parser=parser,
description='Print useful information and exit without processing ' description='Print useful '
'input files.') 'information '
'and exit without processing '
'input files.')
informational_options.add_option('--show-formats', informational_options.add_option(
dest='show_formats', '--show-formats',
action='store_true', dest='show_formats',
default=False, action='store_true',
help='Display helpful information about the ' default=False,
'available output formats.') help='Display helpful information about the available '
'output formats.')
informational_options.add_option('--show-available-tags', informational_options.add_option(
dest='show_tags', '--show-available-tags',
action='store_true', dest='show_tags',
default=False, action='store_true',
help='Display tag mappings for the FMP XML ' default=False,
'output style and exit.') help='Display tag mappings for the FMP XML output style '
'and exit.')
parser.add_option_group(informational_options) parser.add_option_group(informational_options)
print_banner_style(__copyright__) print_banner_style(ptulsconv.__name__)
(options, args) = parser.parse_args(sys.argv) (options, args) = parser.parse_args(sys.argv)
print_section_header_style("Startup") print_section_header_style("Startup")
print_status_style("This run started %s" % (datetime.datetime.now().isoformat())) print_status_style("This run started %s" %
(datetime.datetime.now().isoformat()))
if options.show_tags: if options.show_tags:
dump_field_map() dump_field_map()
@@ -85,15 +97,19 @@ def main():
elif options.show_formats: elif options.show_formats:
dump_formats() dump_formats()
sys.exit(0) sys.exit(0)
if len(args) < 2:
print_fatal_error("Error: No input file")
parser.print_help(sys.stderr)
sys.exit(22)
try: try:
major_mode = options.output_format major_mode = options.output_format
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: except FileNotFoundError as e:
print_fatal_error("Error trying to read input file") print_fatal_error("Error trying to read input file")

View File

@@ -9,13 +9,15 @@ from fractions import Fraction
from typing import Optional, SupportsFloat from typing import Optional, 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. A struct reperesenting a timecode datum.
""" """
def smpte_to_seconds(self, smpte: str) -> Optional[Fraction]: 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) frame_count = smpte_to_frame_count(
smpte, self.logical_fps, drop_frame_hint=self.drop_frame)
if frame_count is None: if frame_count is None:
return None return None
else: else:
@@ -23,29 +25,34 @@ class TimecodeFormat(namedtuple("_TimecodeFormat", "frame_duration logical_fps d
def seconds_to_smpte(self, seconds: SupportsFloat) -> str: def seconds_to_smpte(self, seconds: SupportsFloat) -> str:
frame_count = int(seconds / self.frame_duration) 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) -> Optional[int]:
""" """
Convert a string with a SMPTE timecode representation into a frame count. Convert a string with a SMPTE timecode representation into a frame count.
:param smpte_rep_string: The timecode string :param smpte_rep_string: The timecode string
:param frames_per_logical_second: Num of frames in a logical second. This is asserted to be :param frames_per_logical_second: Num of frames in a logical second. This
in one of `[24,25,30,48,50,60]` is asserted to be in one of `[24,25,30,48,50,60]`
:param drop_frame_hint: `True` if the timecode rep is drop frame. This is ignored (and implied `True`) if :param drop_frame_hint: `True` if the timecode rep is drop frame. This is
the last separator in the timecode string is a semicolon. This is ignored (and implied `False`) if ignored (and implied `True`) if the last separator in the timecode
`frames_per_logical_second` is not 30 or 60. string is a semicolon. This is ignored (and implied `False`) if
`frames_per_logical_second` is not 30 or 60.
""" """
assert frames_per_logical_second in [24, 25, 30, 48, 50, 60] 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: if m is None:
return None return None
hh, mm, ss, sep, ff, frac = m.groups() 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 drop_frame = drop_frame_hint
if sep == ";": if sep == ";":
@@ -54,8 +61,8 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int,
if frames_per_logical_second not in [30, 60]: if frames_per_logical_second not in [30, 60]:
drop_frame = False drop_frame = False
raw_frames = hh * 3600 * frames_per_logical_second + mm * 60 * frames_per_logical_second + \ raw_frames = hh * 3600 * frames_per_logical_second + mm * 60 * \
ss * frames_per_logical_second + ff frames_per_logical_second + ss * frames_per_logical_second + ff
frames = raw_frames frames = raw_frames
if drop_frame is True: if drop_frame is True:
@@ -68,7 +75,8 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int,
return frames return frames
def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, drop_frame: bool = False, 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: Optional[float] = None) -> str:
assert frames_per_logical_second in [24, 25, 30, 48, 50, 60] assert frames_per_logical_second in [24, 25, 30, 48, 50, 60]
assert fractional_frame is None or fractional_frame < 1.0 assert fractional_frame is None or fractional_frame < 1.0
@@ -90,7 +98,8 @@ def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, drop_
hh = hh % 24 hh = hh % 24
if fractional_frame is not None and fractional_frame > 0: 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:]) return "%02i:%02i:%02i%s%02i%s" % (hh, mm, ss, separator, ff,
("%.3f" % fractional_frame)[1:])
else: else:
return "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff) return "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff)

View File

@@ -8,17 +8,20 @@ import os
import sys import sys
from itertools import chain from itertools import chain
import csv import csv
from typing import List from typing import List, Optional, Iterator
from fractions import Fraction from fractions import Fraction
from .docparser.adr_entity import make_entities import ptsl
from .reporting import print_section_header_style, print_status_style, print_warning
from .validations import * 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.docparser import parse_document from ptulsconv.docparser import parse_document
from ptulsconv.docparser.tag_compiler import TagCompiler from ptulsconv.docparser.tag_compiler import TagCompiler
from ptulsconv.broadcast_timecode import TimecodeFormat from ptulsconv.broadcast_timecode import TimecodeFormat
from fractions import Fraction
from ptulsconv.pdf.supervisor_1pg import output_report as output_supervisor_1pg 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.line_count import output_report as output_line_count
@@ -29,7 +32,7 @@ from ptulsconv.pdf.continuity import output_report as output_continuity
from json import JSONEncoder from json import JSONEncoder
class MyEncoder(JSONEncoder): class FractionEncoder(JSONEncoder):
""" """
A subclass of :class:`JSONEncoder` which encodes :class:`Fraction` objects A subclass of :class:`JSONEncoder` which encodes :class:`Fraction` objects
as a dict. as a dict.
@@ -48,9 +51,9 @@ class MyEncoder(JSONEncoder):
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 Writes ADR lines as CSV to the current working directory. Creates
for each character number and name pair, and within that directory, creates directories for each character number and name pair, and within that
a CSV file for each reel. directory, creates a CSV file for each reel.
""" """
reels = set([ln.reel for ln in lines]) reels = set([ln.reel for ln in lines])
@@ -59,12 +62,15 @@ def output_adr_csv(lines: List[ADRLine], time_format: TimecodeFormat):
os.makedirs(dir_name, exist_ok=True) os.makedirs(dir_name, exist_ok=True)
os.chdir(dir_name) os.chdir(dir_name)
for reel in reels: 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: if len(these_lines) == 0:
continue continue
outfile_name = "%s_%s_%s_%s.csv" % (these_lines[0].title, n, these_lines[0].character_name, reel,) 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: with open(outfile_name, mode='w', newline='') as outfile:
writer = csv.writer(outfile, dialect='excel') writer = csv.writer(outfile, dialect='excel')
@@ -78,18 +84,21 @@ def output_adr_csv(lines: List[ADRLine], time_format: TimecodeFormat):
for event in these_lines: for event in these_lines:
this_start = event.start or 0 this_start = event.start or 0
this_finish = event.finish or 0 this_finish = event.finish or 0
this_row = [event.title, event.character_name, event.cue_number, this_row = [event.title, event.character_name,
event.reel, event.version, event.cue_number, event.reel, event.version,
time_format.seconds_to_smpte(this_start), time_format.seconds_to_smpte(this_finish), 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.prompt,
event.reason, event.note, "TV" if event.tv else ""] event.reason, event.note, "TV"
if event.tv else ""]
writer.writerow(this_row) writer.writerow(this_row)
os.chdir("..") os.chdir("..")
def generate_documents(session_tc_format, scenes, adr_lines: Iterator[ADRLine], title): def generate_documents(session_tc_format, scenes, adr_lines: List[ADRLine],
title):
""" """
Create PDF output. Create PDF output.
""" """
@@ -103,22 +112,22 @@ def generate_documents(session_tc_format, scenes, adr_lines: Iterator[ADRLine],
supervisor = next((x.supervisor for x in adr_lines), "") supervisor = next((x.supervisor for x in adr_lines), "")
output_continuity(scenes=scenes, tc_display_format=session_tc_format, output_continuity(scenes=scenes, tc_display_format=session_tc_format,
title=title, client=client, supervisor=supervisor) title=title, client=client or "",
supervisor=supervisor)
# reels = sorted([r for r in compiler.compile_all_time_spans() if r[0] == 'Reel'],
# key=lambda x: x[2])
reels = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6'] reels = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6']
if len(adr_lines) == 0: if len(adr_lines) == 0:
print_status_style("No ADR lines were found in the " print_status_style("No ADR lines were found in the input document. "
"input document. ADR reports will not be generated.") "ADR reports will not be generated.")
else: else:
create_adr_reports(adr_lines, tc_display_format=session_tc_format, create_adr_reports(adr_lines, tc_display_format=session_tc_format,
reel_list=sorted(reels)) 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, Creates a directory heirarchy and a respective set of ADR reports,
given a list of lines. given a list of lines.
@@ -139,7 +148,8 @@ def create_adr_reports(lines: List[ADRLine], tc_display_format: TimecodeFormat,
print_status_style("Creating Director's Logs director and reports") print_status_style("Creating Director's Logs director and reports")
os.makedirs("Director Logs", exist_ok=True) os.makedirs("Director Logs", exist_ok=True)
os.chdir("Director Logs") 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("..") os.chdir("..")
print_status_style("Creating CSV outputs") print_status_style("Creating CSV outputs")
@@ -154,7 +164,7 @@ def create_adr_reports(lines: List[ADRLine], tc_display_format: TimecodeFormat,
output_talent_sides(lines, tc_display_format=tc_display_format) output_talent_sides(lines, tc_display_format=tc_display_format)
def convert(input_file, major_mode, output=sys.stdout, warnings=True): def convert(major_mode, input_file=None, output=sys.stdout, warnings=True):
""" """
Primary worker function, accepts the input file and decides Primary worker function, accepts the input file and decides
what to do with it based on the `major_mode`. what to do with it based on the `major_mode`.
@@ -162,11 +172,28 @@ def convert(input_file, major_mode, output=sys.stdout, warnings=True):
:param input_file: a path to the input file. :param input_file: a path to the input file.
:param major_mode: the selected output mode, 'raw', 'tagged' or 'doc'. :param major_mode: the selected output mode, 'raw', 'tagged' or 'doc'.
""" """
session = parse_document(input_file) session_text = ""
if input_file is not None:
with open(input_file, "r") as file:
session_text = file.read()
else:
with ptsl.open_engine(
company_name="The ptulsconv developers",
application_name="ptulsconv") as engine:
req = engine.export_session_as_text()
req.utf8_encoding()
req.include_track_edls()
req.include_markers()
req.time_type("tc")
req.dont_show_crossfades()
req.selected_tracks_only()
session_text = req.export_string()
session = parse_document(session_text)
session_tc_format = session.header.timecode_format session_tc_format = session.header.timecode_format
if major_mode == 'raw': if major_mode == 'raw':
output.write(MyEncoder().encode(session)) output.write(FractionEncoder().encode(session))
else: else:
compiler = TagCompiler() compiler = TagCompiler()
@@ -174,49 +201,55 @@ def convert(input_file, major_mode, output=sys.stdout, warnings=True):
compiled_events = list(compiler.compile_events()) compiled_events = list(compiler.compile_events())
if major_mode == 'tagged': if major_mode == 'tagged':
output.write(MyEncoder().encode(compiled_events)) output.write(FractionEncoder().encode(compiled_events))
elif major_mode == 'doc': elif major_mode == 'doc':
generic_events, adr_lines = make_entities(compiled_events) generic_events, adr_lines = make_entities(compiled_events)
scenes = sorted([s for s in compiler.compile_all_time_spans() if s[0] == 'Sc'], scenes = sorted([s for s in compiler.compile_all_time_spans()
if s[0] == 'Sc'],
key=lambda x: x[2]) key=lambda x: x[2])
# TODO: Breakdown by titles # TODO: Breakdown by titles
titles = set([x.title for x in (generic_events + adr_lines)]) titles = set([x.title for x in (generic_events + adr_lines)])
if len(titles) != 1: if len(titles) != 1:
print_warning("Multiple titles per export is not supported, " print_warning("Multiple titles per export is not supported, "
"found multiple titles: %s Exiting." % titles) "found multiple titles: %s Exiting." % titles)
exit(-1) exit(-1)
title = list(titles)[0] title = list(titles)[0]
print_status_style("%i generic events found." % len(generic_events)) print_status_style(
"%i generic events found." % len(generic_events)
)
print_status_style("%i ADR events found." % len(adr_lines)) print_status_style("%i ADR events found." % len(adr_lines))
if warnings: if warnings:
perform_adr_validations(adr_lines) 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]): def perform_adr_validations(lines: Iterator[ADRLine]):
""" """
Performs validations on the input. Performs validations on the input.
""" """
for warning in chain(validate_unique_field(lines, for warning in chain(
field='cue_number', validate_unique_field(lines,
scope='title'), field='cue_number',
validate_non_empty_field(lines, scope='title'),
field='cue_number'), validate_non_empty_field(lines,
validate_non_empty_field(lines, field='cue_number'),
field='character_id'), validate_non_empty_field(lines,
validate_non_empty_field(lines, field='character_id'),
field='title'), validate_non_empty_field(lines,
validate_dependent_value(lines, field='title'),
key_field='character_id', validate_dependent_value(lines,
dependent_field='character_name'), key_field='character_id',
validate_dependent_value(lines, dependent_field='character_name'),
key_field='character_id', validate_dependent_value(lines,
dependent_field='actor_name')): key_field='character_id',
dependent_field='actor_name')):
print_warning(warning.report_message()) print_warning(warning.report_message())

View File

@@ -1,6 +1,6 @@
""" """
This module defines classes and methods for converting :class:`Event` objects into This module defines classes and methods for converting :class:`Event` objects
:class:`ADRLine` objects. into :class:`ADRLine` objects.
""" """
from ptulsconv.docparser.tag_compiler import Event from ptulsconv.docparser.tag_compiler import Event
@@ -11,15 +11,16 @@ from fractions import Fraction
from ptulsconv.docparser.tag_mapping import TagMapping from ptulsconv.docparser.tag_mapping import TagMapping
def make_entities(from_events: List[Event]) -> Tuple[List['GenericEvent'], List['ADRLine']]: 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 Accepts a list of Events and converts them into either ADRLine events or
GenricEvents by calling :func:`make_entity` on each member. GenricEvents by calling :func:`make_entity` on each member.
:param from_events: A list of `Event` objects. :param from_events: A list of `Event` objects.
:returns: A tuple of two lists, the first containing :class:`GenericEvent` and the :returns: A tuple of two lists, the first containing :class:`GenericEvent`
second containing :class:`ADRLine`. and the second containing :class:`ADRLine`.
""" """
generic_events = list() generic_events = list()
adr_lines = list() adr_lines = list()
@@ -67,14 +68,15 @@ class GenericEvent:
scene: Optional[str] = None scene: Optional[str] = None
version: Optional[str] = None version: Optional[str] = None
reel: Optional[str] = None reel: Optional[str] = None
start: Fraction = Fraction(0,1) start: Fraction = Fraction(0, 1)
finish: Fraction = Fraction(0,1) finish: Fraction = Fraction(0, 1)
omitted: bool = False omitted: bool = False
note: Optional[str] = None note: Optional[str] = None
requested_by: Optional[str] = None requested_by: Optional[str] = None
tag_mapping = [ tag_mapping = [
TagMapping(source='Title', target="title", alt=TagMapping.ContentSource.Session), TagMapping(source='Title', target="title",
alt=TagMapping.ContentSource.Session),
TagMapping(source="Supv", target="supervisor"), TagMapping(source="Supv", target="supervisor"),
TagMapping(source="Client", target="client"), TagMapping(source="Client", target="client"),
TagMapping(source="Sc", target="scene"), TagMapping(source="Sc", target="scene"),
@@ -111,9 +113,11 @@ class ADRLine(GenericEvent):
TagMapping(source="P", target="priority"), TagMapping(source="P", target="priority"),
TagMapping(source="QN", target="cue_number"), TagMapping(source="QN", target="cue_number"),
TagMapping(source="CN", target="character_id"), TagMapping(source="CN", target="character_id"),
TagMapping(source="Char", target="character_name", alt=TagMapping.ContentSource.Track), TagMapping(source="Char", target="character_name",
alt=TagMapping.ContentSource.Track),
TagMapping(source="Actor", target="actor_name"), TagMapping(source="Actor", target="actor_name"),
TagMapping(source="Line", target="prompt", alt=TagMapping.ContentSource.Clip), TagMapping(source="Line", target="prompt",
alt=TagMapping.ContentSource.Clip),
TagMapping(source="R", target="reason"), TagMapping(source="R", target="reason"),
TagMapping(source="Mins", target="time_budget_mins", TagMapping(source="Mins", target="time_budget_mins",
formatter=(lambda n: float(n))), formatter=(lambda n: float(n))),
@@ -131,5 +135,3 @@ class ADRLine(GenericEvent):
TagMapping(source="OPT", target="optional", TagMapping(source="OPT", target="optional",
formatter=(lambda x: len(x) > 0)) formatter=(lambda x: len(x) > 0))
] ]

View File

@@ -19,21 +19,41 @@ class SessionDescriptor:
self.tracks = kwargs['tracks'] self.tracks = kwargs['tracks']
self.markers = kwargs['markers'] self.markers = kwargs['markers']
def markers_timed(self) -> Iterator[Tuple['MarkerDescriptor', Fraction]]: 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: for marker in self.markers:
marker_time = Fraction(marker.time_reference, int(self.header.sample_rate))
#marker_time = self.header.convert_timecode(marker.location) 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 yield marker, marker_time
def tracks_clips(self) -> Iterator[Tuple['TrackDescriptor', 'TrackClipDescriptor']]: def tracks_clips(self) -> Iterator[Tuple['TrackDescriptor',
'TrackClipDescriptor']]:
"""
Iterate each track clip with its respective owning clip.
"""
for track in self.tracks: for track in self.tracks:
for clip in track.clips: for clip in track.clips:
yield track, clip yield track, clip
def track_clips_timed(self) -> Iterator[Tuple["TrackDescriptor", "TrackClipDescriptor", def track_clips_timed(self) -> Iterator[Tuple["TrackDescriptor",
Fraction, Fraction, Fraction]]: "TrackClipDescriptor",
Fraction, Fraction, Fraction]
]:
""" """
:return: A Generator that yields track, clip, start time, finish time, and timestamp 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(): for track, clip in self.tracks_clips():
start_time = self.header.convert_timecode(clip.start_timecode) start_time = self.header.convert_timecode(clip.start_timecode)
@@ -105,10 +125,12 @@ class HeaderDescriptor:
if self.timecode_fps in frame_rates.keys(): if self.timecode_fps in frame_rates.keys():
return frame_rates[self.timecode_fps] return frame_rates[self.timecode_fps]
else: else:
raise ValueError("Unrecognized TC rate (%s)" % self.timecode_format) raise ValueError("Unrecognized TC rate (%s)" %
self.timecode_format)
class TrackDescriptor: class TrackDescriptor:
index: int
name: str name: str
comments: str comments: str
user_delay_samples: int user_delay_samples: int
@@ -117,6 +139,7 @@ class TrackDescriptor:
clips: List["TrackClipDescriptor"] clips: List["TrackClipDescriptor"]
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.index = kwargs['index']
self.name = kwargs['name'] self.name = kwargs['name']
self.comments = kwargs['comments'] self.comments = kwargs['comments']
self.user_delay_samples = kwargs['user_delay_samples'] self.user_delay_samples = kwargs['user_delay_samples']
@@ -165,6 +188,7 @@ class MarkerDescriptor:
units: str units: str
name: str name: str
comments: str comments: str
track_marker: bool
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.number = kwargs['number'] self.number = kwargs['number']
@@ -173,3 +197,4 @@ class MarkerDescriptor:
self.units = kwargs['units'] self.units = kwargs['units']
self.name = kwargs['name'] self.name = kwargs['name']
self.comments = kwargs['comments'] self.comments = kwargs['comments']
self.track_marker = kwargs['track_marker']

View File

@@ -1 +1 @@
from dataclasses import dataclass # from dataclasses import dataclass

View File

@@ -1,13 +1,16 @@
from parsimonious.nodes import NodeVisitor from parsimonious.nodes import NodeVisitor
from parsimonious.grammar import Grammar from parsimonious.grammar import Grammar
from .doc_entity import SessionDescriptor, HeaderDescriptor, TrackDescriptor, FileDescriptor, \ from .doc_entity import SessionDescriptor, HeaderDescriptor, TrackDescriptor, \
TrackClipDescriptor, ClipDescriptor, PluginDescriptor, MarkerDescriptor FileDescriptor, TrackClipDescriptor, ClipDescriptor, PluginDescriptor, \
MarkerDescriptor
protools_text_export_grammar = Grammar( protools_text_export_grammar = Grammar(
r""" r"""
document = header files_section? clips_section? plugin_listing? track_listing? markers_listing? document = header files_section? clips_section? plugin_listing?
track_listing? markers_block?
header = "SESSION NAME:" fs string_value rs header = "SESSION NAME:" fs string_value rs
"SAMPLE RATE:" fs float_value rs "SAMPLE RATE:" fs float_value rs
"BIT DEPTH:" fs integer_value "-bit" rs "BIT DEPTH:" fs integer_value "-bit" rs
@@ -17,21 +20,29 @@ protools_text_export_grammar = Grammar(
"# OF AUDIO CLIPS:" fs integer_value rs "# OF AUDIO CLIPS:" fs integer_value rs
"# OF AUDIO FILES:" fs integer_value rs block_ending "# OF AUDIO FILES:" fs integer_value rs block_ending
frame_rate = ("60" / "59.94" / "30" / "29.97" / "25" / "24" / "23.976") frame_rate = ("60" / "59.94" / "30" / "29.97" / "25" / "24" /
files_section = files_header files_column_header file_record* block_ending "23.976")
files_section = files_header files_column_header file_record*
block_ending
files_header = "F I L E S I N S E S S I O N" rs files_header = "F I L E S I N S E S S I O N" rs
files_column_header = "Filename" isp fs "Location" rs files_column_header = "Filename" isp fs "Location" rs
file_record = string_value fs string_value rs file_record = string_value fs string_value rs
clips_section = clips_header clips_column_header clip_record* block_ending clips_section = clips_header clips_column_header clip_record*
block_ending
clips_header = "O N L I N E C L I P S I N S E S S I O N" rs clips_header = "O N L I N E C L I P S I N S E S S I O N" rs
clips_column_header = string_value fs string_value rs clips_column_header = string_value fs string_value rs
clip_record = string_value fs string_value (fs "[" integer_value "]")? rs clip_record = string_value fs string_value
(fs "[" integer_value "]")? rs
plugin_listing = plugin_header plugin_column_header plugin_record* block_ending plugin_listing = plugin_header plugin_column_header plugin_record*
block_ending
plugin_header = "P L U G - I N S L I S T I N G" rs plugin_header = "P L U G - I N S L I S T I N G" rs
plugin_column_header = "MANUFACTURER " fs "PLUG-IN NAME " fs plugin_column_header = "MANUFACTURER " fs
"VERSION " fs "FORMAT " fs "STEMS " fs "PLUG-IN NAME " fs
"VERSION " fs
"FORMAT " fs
"STEMS " fs
"NUMBER OF INSTANCES" rs "NUMBER OF INSTANCES" rs
plugin_record = string_value fs string_value fs string_value fs plugin_record = string_value fs string_value fs string_value fs
string_value fs string_value fs string_value rs string_value fs string_value fs string_value rs
@@ -45,8 +56,10 @@ protools_text_export_grammar = Grammar(
"USER DELAY:" fs integer_value " Samples" rs "USER DELAY:" fs integer_value " Samples" rs
"STATE: " track_state_list rs "STATE: " track_state_list rs
("PLUG-INS: " ( fs string_value )* rs)? ("PLUG-INS: " ( fs string_value )* rs)?
"CHANNEL " fs "EVENT " fs "CLIP NAME " fs "CHANNEL " fs "EVENT " fs
"START TIME " fs "END TIME " fs "DURATION " fs "CLIP NAME " fs
"START TIME " fs "END TIME " fs
"DURATION " fs
("TIMESTAMP " fs)? "STATE" rs ("TIMESTAMP " fs)? "STATE" rs
track_state_list = (track_state " ")* track_state_list = (track_state " ")*
@@ -56,18 +69,39 @@ protools_text_export_grammar = Grammar(
track_clip_entry = integer_value isp fs track_clip_entry = integer_value isp fs
integer_value isp fs integer_value isp fs
string_value fs string_value fs
string_value fs string_value fs string_value fs (string_value fs)? string_value fs string_value fs string_value fs
(string_value fs)?
track_clip_state rs track_clip_state rs
track_clip_state = ("Muted" / "Unmuted") track_clip_state = ("Muted" / "Unmuted")
markers_listing = markers_listing_header markers_column_header marker_record* markers_block = markers_block_header
markers_listing_header = "M A R K E R S L I S T I N G" rs (markers_list / markers_list_simple)
markers_column_header = "# " fs "LOCATION " fs "TIME REFERENCE " fs
"UNITS " fs "NAME " fs "COMMENTS" rs markers_list_simple = markers_column_header_simple marker_record_simple*
markers_list = markers_column_header marker_record*
markers_block_header = "M A R K E R S L I S T I N G" rs
markers_column_header_simple =
"# LOCATION TIME REFERENCE "
"UNITS NAME "
"COMMENTS" rs
markers_column_header =
"# LOCATION TIME REFERENCE "
"UNITS NAME "
"TRACK NAME "
"TRACK TYPE COMMENTS" rs
marker_record_simple = integer_value isp fs string_value fs
integer_value isp fs string_value fs string_value
fs string_value rs
marker_record = integer_value isp fs string_value fs integer_value isp fs marker_record = integer_value isp fs string_value fs integer_value isp fs
string_value fs string_value fs string_value rs string_value fs string_value fs string_value fs
string_value fs string_value rs
fs = "\t" fs = "\t"
rs = "\n" rs = "\n"
@@ -79,21 +113,24 @@ protools_text_export_grammar = Grammar(
""") """)
def parse_document(path: str) -> SessionDescriptor: def parse_document(session_text: str) -> SessionDescriptor:
""" """
Parse a Pro Tools text export. Parse a Pro Tools text export.
:param path: path to a file :param session_text: Pro Tools session text export
:return: the session descriptor :return: the session descriptor
""" """
with open(path, 'r') as f: ast = protools_text_export_grammar.parse(session_text)
ast = protools_text_export_grammar.parse(f.read()) return DocParserVisitor().visit(ast)
return DocParserVisitor().visit(ast)
class DocParserVisitor(NodeVisitor): class DocParserVisitor(NodeVisitor):
@staticmethod def __init__(self):
def visit_document(_, visited_children) -> SessionDescriptor: self.track_index = 0
# @staticmethod
def visit_document(self, _, visited_children) -> SessionDescriptor:
self.track_index = 0
files = next(iter(visited_children[1]), None) files = next(iter(visited_children[1]), None)
clips = next(iter(visited_children[2]), None) clips = next(iter(visited_children[2]), None)
plugins = next(iter(visited_children[3]), None) plugins = next(iter(visited_children[3]), None)
@@ -126,27 +163,32 @@ class DocParserVisitor(NodeVisitor):
@staticmethod @staticmethod
def visit_files_section(_, visited_children): def visit_files_section(_, visited_children):
return list(map(lambda child: FileDescriptor(filename=child[0], path=child[2]), visited_children[2])) return list(map(
lambda child: FileDescriptor(filename=child[0], path=child[2]),
visited_children[2]))
@staticmethod @staticmethod
def visit_clips_section(_, visited_children): def visit_clips_section(_, visited_children):
channel = next(iter(visited_children[2][3]), 1) channel = next(iter(visited_children[2][3]), 1)
return list(map(lambda child: ClipDescriptor(clip_name=child[0], file=child[2], channel=channel), return list(map(
visited_children[2])) lambda child: ClipDescriptor(clip_name=child[0], file=child[2],
channel=channel),
visited_children[2]))
@staticmethod @staticmethod
def visit_plugin_listing(_, visited_children): def visit_plugin_listing(_, visited_children):
return list(map(lambda child: PluginDescriptor(manufacturer=child[0], return list(map(lambda child:
plugin_name=child[2], PluginDescriptor(manufacturer=child[0],
version=child[4], plugin_name=child[2],
format=child[6], version=child[4],
stems=child[8], format=child[6],
count_instances=child[10]), stems=child[8],
count_instances=child[10]),
visited_children[2])) visited_children[2]))
@staticmethod # @staticmethod
def visit_track_block(_, visited_children): def visit_track_block(self, _, visited_children):
track_header, track_clip_list = visited_children track_header, track_clip_list = visited_children
clips = [] clips = []
for clip in track_clip_list: for clip in track_clip_list:
@@ -158,7 +200,11 @@ class DocParserVisitor(NodeVisitor):
for plugin in plugin_opt[1]: for plugin in plugin_opt[1]:
plugins.append(plugin[1]) plugins.append(plugin[1])
this_index = self.track_index
self.track_index += 1
return TrackDescriptor( return TrackDescriptor(
index=this_index,
name=track_header[2], name=track_header[2],
comments=track_header[6], comments=track_header[6],
user_delay_samples=track_header[10], user_delay_samples=track_header[10],
@@ -202,22 +248,37 @@ class DocParserVisitor(NodeVisitor):
return node.text return node.text
@staticmethod @staticmethod
def visit_markers_listing(_, visited_children): def visit_markers_block(_, visited_children):
markers = [] markers = []
for marker in visited_children[2]: for marker in visited_children[1][0][1]:
markers.append(marker) markers.append(marker)
return markers return markers
@staticmethod @staticmethod
def visit_marker_record(_, visited_children): def visit_marker_record_simple(_, visited_children):
return MarkerDescriptor(number=visited_children[0], return MarkerDescriptor(number=visited_children[0],
location=visited_children[3], location=visited_children[3],
time_reference=visited_children[5], time_reference=visited_children[5],
units=visited_children[8], units=visited_children[8],
name=visited_children[10], name=visited_children[10],
comments=visited_children[12]) comments=visited_children[12],
track_marker=False)
@staticmethod
def visit_marker_record(_, visited_children):
track_type = visited_children[15]
is_track_marker = (track_type == "Track")
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)
@staticmethod @staticmethod
def visit_formatted_clip_name(_, visited_children): def visit_formatted_clip_name(_, visited_children):

View File

@@ -24,20 +24,25 @@ class TagCompiler:
items. items.
""" """
Intermediate = namedtuple('Intermediate', 'track_content track_tags track_comment_tags ' Intermediate = namedtuple('Intermediate',
'clip_content clip_tags clip_tag_mode start finish') 'track_content track_tags track_comment_tags '
'clip_content clip_tags clip_tag_mode start '
'finish')
session: doc_entity.SessionDescriptor 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) :returns: A `List` of (key: str, value: str, start: Fraction,
finish: Fraction)
""" """
ret_list = list() ret_list = list()
for element in self.parse_data(): for element in self.parse_data():
if element.clip_tag_mode == TagPreModes.TIMESPAN: if element.clip_tag_mode == TagPreModes.TIMESPAN:
for k in element.clip_tags.keys(): for k in element.clip_tags.keys():
ret_list.append((k, element.clip_tags[k], element.start, element.finish)) ret_list.append((k, element.clip_tags[k], element.start,
element.finish))
return ret_list return ret_list
@@ -73,26 +78,31 @@ class TagCompiler:
step3 = self.collect_time_spans(step2) step3 = self.collect_time_spans(step2)
step4 = self.apply_tags(step3) step4 = self.apply_tags(step3)
for datum in step4: for datum in step4:
yield Event(clip_name=datum[0], track_name=datum[1], session_name=datum[2], yield Event(clip_name=datum[0], track_name=datum[1],
tags=datum[3], start=datum[4], finish=datum[5]) session_name=datum[2], tags=datum[3], start=datum[4],
finish=datum[5])
def _marker_tags(self, at): def _marker_tags(self, at):
retval = dict() retval = dict()
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]): for marker, _ in sorted(applicable, key=lambda x: x[1]):
retval.update(parse_tags(marker.comments or "").tag_dict) retval.update(parse_tags(marker.comments or "").tag_dict)
retval.update(parse_tags(marker.name or "").tag_dict) retval.update(parse_tags(marker.name or "").tag_dict)
return retval 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: for clip in clips:
if clip.clip_tag_mode == 'Directive': if clip.clip_tag_mode == 'Directive':
continue continue
else: else:
yield clip yield clip
@staticmethod @staticmethod
def _coalesce_tags(clip_tags: dict, track_tags: dict, def _coalesce_tags(clip_tags: dict, track_tags: dict,
track_comment_tags: dict, track_comment_tags: dict,
@@ -117,29 +127,33 @@ class TagCompiler:
track_comments_parsed = parse_tags(track.comments) track_comments_parsed = parse_tags(track.comments)
clip_parsed = parse_tags(clip.clip_name) clip_parsed = parse_tags(clip.clip_name)
yield TagCompiler.Intermediate(track_content=track_parsed.content, yield TagCompiler.Intermediate(
track_tags=track_parsed.tag_dict, track_content=track_parsed.content,
track_comment_tags=track_comments_parsed.tag_dict, track_tags=track_parsed.tag_dict,
clip_content=clip_parsed.content, track_comment_tags=track_comments_parsed.tag_dict,
clip_tags=clip_parsed.tag_dict, clip_content=clip_parsed.content,
clip_tag_mode=clip_parsed.mode, clip_tags=clip_parsed.tag_dict,
start=start, finish=finish) clip_tag_mode=clip_parsed.mode,
start=start, finish=finish)
@staticmethod @staticmethod
def apply_appends(parsed: Iterator[Intermediate]) -> Iterator[Intermediate]: def apply_appends(parsed: Iterator[Intermediate]) -> \
Iterator[Intermediate]:
def should_append(a, b): 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): def do_append(a, b):
merged_tags = dict(a.clip_tags) merged_tags = dict(a.clip_tags)
merged_tags.update(b.clip_tags) merged_tags.update(b.clip_tags)
return TagCompiler.Intermediate(track_content=a.track_content, return TagCompiler.Intermediate(
track_tags=a.track_tags, track_content=a.track_content,
track_comment_tags=a.track_comment_tags, track_tags=a.track_tags,
clip_content=a.clip_content + ' ' + b.clip_content, track_comment_tags=a.track_comment_tags,
clip_tags=merged_tags, clip_tag_mode=a.clip_tag_mode, clip_content=a.clip_content + ' ' + b.clip_content,
start=a.start, finish=b.finish) 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) yield from apply_appends(parsed, should_append, do_append)
@@ -158,12 +172,14 @@ class TagCompiler:
@staticmethod @staticmethod
def _time_span_tags(at_time: Fraction, applicable_spans) -> dict: def _time_span_tags(at_time: Fraction, applicable_spans) -> dict:
retval = dict() retval = dict()
for tags in reversed([a[0] for a in applicable_spans if a[1] <= at_time <= a[2]]): for tags in reversed([a[0] for a in applicable_spans
if a[1] <= at_time <= a[2]]):
retval.update(tags) retval.update(tags)
return retval 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) session_parsed = parse_tags(self.session.header.session_name)
@@ -171,14 +187,16 @@ class TagCompiler:
event: 'TagCompiler.Intermediate' event: 'TagCompiler.Intermediate'
marker_tags = self._marker_tags(event.start) marker_tags = self._marker_tags(event.start)
time_span_tags = self._time_span_tags(event.start, time_spans) time_span_tags = self._time_span_tags(event.start, time_spans)
tags = self._coalesce_tags(clip_tags=event.clip_tags, tags = self._coalesce_tags(
track_tags=event.track_tags, clip_tags=event.clip_tags,
track_comment_tags=event.track_comment_tags, track_tags=event.track_tags,
timespan_tags=time_span_tags, track_comment_tags=event.track_comment_tags,
marker_tags=marker_tags, timespan_tags=time_span_tags,
session_tags=session_parsed.tag_dict) marker_tags=marker_tags,
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, def apply_appends(source: Iterator,

View File

@@ -48,7 +48,8 @@ class TagMapping:
for rule in rules: for rule in rules:
if rule.target in done: if rule.target in done:
continue 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) done.update(rule.target)
def __init__(self, source: str, def __init__(self, source: str,

View File

@@ -1,5 +1,5 @@
from parsimonious import NodeVisitor, Grammar from parsimonious import NodeVisitor, Grammar
from typing import Dict, Union from typing import Dict
from enum import Enum from enum import Enum
@@ -52,8 +52,9 @@ class TagListVisitor(NodeVisitor):
modifier_opt, line_opt, _, tag_list_opt = visited_children 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()), tag_dict=next(iter(tag_list_opt), dict()),
mode=TagPreModes(next(iter(modifier_opt), 'Normal')) mode=TagPreModes(
next(iter(modifier_opt), 'Normal'))
) )
@staticmethod @staticmethod

View File

@@ -1,12 +1,14 @@
#import ffmpeg # ffmpeg-python # import ffmpeg # ffmpeg-python
# TODO: Implement movie export # TODO: Implement movie export
# def create_movie(event): # def create_movie(event):
# start = event['Movie.Start_Offset_Seconds'] # start = event['Movie.Start_Offset_Seconds']
# duration = event['PT.Clip.Finish_Seconds'] - event['PT.Clip.Start_Seconds'] # duration = event['PT.Clip.Finish_Seconds'] -
# event['PT.Clip.Start_Seconds']
# input_movie = event['Movie.Filename'] # input_movie = event['Movie.Filename']
# print("Will make movie starting at {}, dur {} from movie {}".format(start, duration, input_movie)) # print("Will make movie starting at {}, dur {} from movie {}"
# .format(start, duration, input_movie))
# #
# #
# def export_movies(events): # def export_movies(events):

View File

@@ -17,6 +17,8 @@ from typing import List
# This is from https://code.activestate.com/recipes/576832/ for # This is from https://code.activestate.com/recipes/576832/ for
# generating page count messages # generating page count messages
class ReportCanvas(canvas.Canvas): class ReportCanvas(canvas.Canvas):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
canvas.Canvas.__init__(self, *args, **kwargs) canvas.Canvas.__init__(self, *args, **kwargs)
@@ -38,10 +40,12 @@ class ReportCanvas(canvas.Canvas):
def draw_page_number(self, page_count): def draw_page_number(self, page_count):
self.saveState() self.saveState()
self.setFont('Helvetica', 10) #FIXME make this customizable 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.drawString(0.5 * inch, 0.5 * inch,
"Page %d of %d" % (self._pageNumber, page_count))
right_edge = self._pagesize[0] - 0.5 * inch 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 = self.beginPath()
top_line.moveTo(0.5 * inch, 0.75 * inch) top_line.moveTo(0.5 * inch, 0.75 * inch)
@@ -76,13 +80,15 @@ def make_doc_template(page_size, filename, document_title,
title_box, report_box = header_box.split_x(3.5 * inch, direction='r') title_box, report_box = header_box.split_x(3.5 * inch, direction='r')
on_page_lambda = (lambda c, _: on_page_lambda = (lambda c, _:
draw_header_footer(c, report_box, title_box, draw_header_footer(c, report_box, title_box,
footer_box,title=title, footer_box, title=title,
supervisor=supervisor, supervisor=supervisor,
document_subheader=document_subheader, document_subheader=document_subheader,
client=client, doc_title=document_header)) client=client,
doc_title=document_header))
frames = [Frame(page_box.min_x, page_box.min_y, page_box.width, page_box.height)] frames = [Frame(page_box.min_x, page_box.min_y,
page_box.width, page_box.height)]
page_template = PageTemplate(id="Main", page_template = PageTemplate(id="Main",
frames=frames, frames=frames,
@@ -119,12 +125,17 @@ def time_format(mins, zero_str="-"):
return "%i:%02i" % (hh, mm) return "%i:%02i" % (hh, mm)
def draw_header_footer(a_canvas: ReportCanvas, left_box, right_box, footer_box, title: str, supervisor: str, def draw_header_footer(a_canvas: ReportCanvas, left_box, right_box,
document_subheader: str, client: str, doc_title="", font_name='Helvetica'): 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., ]) (_supervisor_box, client_box,), title_box = \
title_box.draw_text_cell(a_canvas, title, font_name, 18, inset_y=2., inset_x=5.) right_box.divide_y([16., 16., ])
client_box.draw_text_cell(a_canvas, client, font_name, 11, inset_y=2., inset_x=5.) 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.)
a_canvas.saveState() a_canvas.saveState()
a_canvas.setLineWidth(0.5) a_canvas.setLineWidth(0.5)
@@ -139,16 +150,20 @@ def draw_header_footer(a_canvas: ReportCanvas, left_box, right_box, footer_box,
a_canvas.drawPath(tline2) a_canvas.drawPath(tline2)
a_canvas.restoreState() 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., 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.,
inset_y=2.)
if document_subheader is not None: 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., inset_y=2.)
if supervisor is not None: if supervisor is not None:
a_canvas.setFont(font_name, 11.) a_canvas.setFont(font_name, 11.)
a_canvas.drawCentredString(footer_box.min_x + footer_box.width / 2., footer_box.min_y, supervisor) a_canvas.drawCentredString(footer_box.min_x + footer_box.width / 2.,
footer_box.min_y, supervisor)
class GRect: class GRect:
@@ -201,10 +216,12 @@ class GRect:
else: else:
if direction == 'l': if direction == 'l':
return (GRect(self.min_x, self.min_y, at, self.height), return (GRect(self.min_x, self.min_y, at, self.height),
GRect(self.min_x + at, self.y, self.width - at, self.height)) GRect(self.min_x + at, self.y,
self.width - at, self.height))
else: else:
return (GRect(self.max_x - at, self.y, 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)) 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: if at >= self.height:
@@ -214,19 +231,23 @@ class GRect:
else: else:
if direction == 'u': if direction == 'u':
return (GRect(self.x, self.y, self.width, at), return (GRect(self.x, self.y, self.width, at),
GRect(self.x, self.y + at, self.width, self.height - at)) GRect(self.x, self.y + at,
self.width, self.height - at))
else: else:
return (GRect(self.x, self.max_y - at, self.width, at), return (GRect(self.x, self.max_y - at, self.width, at),
GRect(self.x, self.y, self.width, self.height - at)) GRect(self.x, self.y,
self.width, self.height - at))
def inset_xy(self, dx, dy): 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): def inset(self, d):
return self.inset_xy(d, d) return self.inset_xy(d, d)
def __repr__(self): def __repr__(self):
return "<GRect x=%f y=%f width=%f height=%f>" % (self.x, self.y, self.width, self.height) return "<GRect x=%f y=%f width=%f height=%f>" % \
(self.x, self.y, self.width, self.height)
def divide_x(self, x_list, direction='l'): def divide_x(self, x_list, direction='l'):
ret_list = list() ret_list = list()
@@ -259,13 +280,17 @@ class GRect:
def draw_border_impl(en): def draw_border_impl(en):
if en == 'min_x': if en == 'min_x':
coordinates = ((self.min_x, self.min_y), (self.min_x, self.max_y)) coordinates = ((self.min_x, self.min_y),
(self.min_x, self.max_y))
elif en == 'max_x': elif en == 'max_x':
coordinates = ((self.max_x, self.min_y), (self.max_x, self.max_y)) coordinates = ((self.max_x, self.min_y),
(self.max_x, self.max_y))
elif en == 'min_y': elif en == 'min_y':
coordinates = ((self.min_x, self.min_y), (self.max_x, self.min_y)) coordinates = ((self.min_x, self.min_y),
(self.max_x, self.min_y))
elif en == 'max_y': elif en == 'max_y':
coordinates = ((self.min_x, self.max_y), (self.max_x, self.max_y)) coordinates = ((self.min_x, self.max_y),
(self.max_x, self.max_y))
else: else:
return return

View File

@@ -4,7 +4,7 @@ from typing import Tuple, List
from reportlab.lib.pagesizes import portrait, letter from reportlab.lib.pagesizes import portrait, letter
from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch from reportlab.lib.units import inch
from reportlab.platypus import Paragraph, Table, Spacer from reportlab.platypus import Paragraph, Table
from ptulsconv.broadcast_timecode import TimecodeFormat from ptulsconv.broadcast_timecode import TimecodeFormat
from ptulsconv.pdf import make_doc_template from ptulsconv.pdf import make_doc_template
@@ -12,14 +12,15 @@ from ptulsconv.pdf import make_doc_template
# TODO: A Continuity # TODO: A Continuity
def table_for_scene(scene, tc_format, font_name = 'Helvetica'): def table_for_scene(scene, tc_format, font_name='Helvetica'):
scene_style = getSampleStyleSheet()['Normal'] scene_style = getSampleStyleSheet()['Normal']
scene_style.fontName = font_name scene_style.fontName = font_name
scene_style.leftIndent = 0. scene_style.leftIndent = 0.
scene_style.leftPadding = 0. scene_style.leftPadding = 0.
scene_style.spaceAfter = 18. 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])) tc_data = "<em>%s</em><br />%s" % (tc_format.seconds_to_smpte(scene[2]),
tc_format.seconds_to_smpte(scene[3]))
row = [ row = [
Paragraph(tc_data, scene_style), Paragraph(tc_data, scene_style),
@@ -36,7 +37,7 @@ def table_for_scene(scene, tc_format, font_name = 'Helvetica'):
def output_report(scenes: List[Tuple[str, str, Fraction, Fraction]], def output_report(scenes: List[Tuple[str, str, Fraction, Fraction]],
tc_display_format: TimecodeFormat, tc_display_format: TimecodeFormat,
title: str, client: str, supervisor, paper_size = letter): title: str, client: str, supervisor, paper_size=letter):
filename = "%s Continuity.pdf" % title filename = "%s Continuity.pdf" % title
document_header = "Continuity" document_header = "Continuity"

View File

@@ -1,7 +1,7 @@
from typing import List, Optional from typing import List, Optional
from reportlab.pdfbase import pdfmetrics # from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont # from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.units import inch from reportlab.lib.units import inch
from reportlab.lib.pagesizes import letter, portrait from reportlab.lib.pagesizes import letter, portrait
@@ -14,9 +14,12 @@ from .__init__ import time_format, make_doc_template
from ..docparser.adr_entity import ADRLine from ..docparser.adr_entity import ADRLine
def build_columns(lines: List[ADRLine], reel_list: Optional[List[str]], show_priorities=False, include_omitted=False): def build_columns(lines: List[ADRLine], reel_list: Optional[List[str]],
show_priorities=False, include_omitted=False):
columns = list() columns = list()
reel_numbers = reel_list or sorted(set([x.reel for x in lines if x.reel is not None])) 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 num_column_width = 15. / 32. * inch
@@ -33,7 +36,10 @@ def build_columns(lines: List[ADRLine], reel_list: Optional[List[str]], show_pri
'heading': 'Role', 'heading': 'Role',
'value_getter': lambda recs: recs[0].character_name, 'value_getter': lambda recs: recs[0].character_name,
'value_getter2': lambda recs: recs[0].actor_name or "", '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)], 'style_getter': lambda col_index: [('LINEAFTER',
(col_index, 0),
(col_index, -1),
1.0, colors.black)],
'width': 1.75 * inch, 'width': 1.75 * inch,
'summarize': False 'summarize': False
}) })
@@ -41,30 +47,48 @@ def build_columns(lines: List[ADRLine], reel_list: Optional[List[str]], show_pri
columns.append({ columns.append({
'heading': 'TV', 'heading': 'TV',
'value_getter': lambda recs: len([r for r in recs if r.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. 'value_getter2': (lambda recs:
for r in recs if r.tv])), time_format(sum([r.time_budget_mins or 0.
'style_getter': lambda col_index: [('ALIGN', (col_index, 0), (col_index, -1), 'CENTER'), for r in recs if r.tv]))
('LINEBEFORE', (col_index, 0), (col_index, -1), 1., colors.black), ),
('LINEAFTER', (col_index, 0), (col_index, -1), .5, colors.gray)], '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 'width': num_column_width
}) })
columns.append({ columns.append({
'heading': 'Opt', 'heading': 'Opt',
'value_getter': lambda recs: len([r for r in recs if r.optional]), '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. 'value_getter2': (lambda recs:
for r in recs if r.optional])), time_format(sum([r.time_budget_mins or 0.
'style_getter': lambda col_index: [('ALIGN', (col_index, 0), (col_index, -1), 'CENTER'), for r in recs if r.optional]))
('LINEAFTER', (col_index, 0), (col_index, -1), .5, colors.gray)], ),
'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 'width': num_column_width
}) })
columns.append({ columns.append({
'heading': 'Eff', 'heading': 'Eff',
'value_getter': lambda recs: len([r for r in recs if r.effort]), '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. 'value_getter2': (lambda recs:
for r in recs if r.effort])), time_format(sum([r.time_budget_mins or 0.
'style_getter': lambda col_index: [('ALIGN', (col_index, 0), (col_index, -1), 'CENTER')], for r in recs if r.effort]))
),
'style_getter': (lambda col_index:
[('ALIGN', (col_index, 0), (col_index, -1),
'CENTER')]
),
'width': num_column_width 'width': num_column_width
}) })
@@ -80,23 +104,26 @@ def build_columns(lines: List[ADRLine], reel_list: Optional[List[str]], show_pri
}) })
if len(reel_numbers) > 0: if len(reel_numbers) > 0:
# columns.append({
# 'heading': 'RX',
# 'value_getter': lambda recs: blank_len([r for r in recs if 'Reel' not in r.keys()]),
# 'value_getter2': lambda recs: time_format(sum([r.get('Time Budget Mins', 0.) for r in recs
# if 'Reel' not in r.keys()])),
# 'style_getter': lambda col_index: [('ALIGN', (col_index, 0), (col_index, -1), 'CENTER')],
# 'width': num_column_width
# })
for n in reel_numbers: for n in reel_numbers:
columns.append({ columns.append({
'heading': n, 'heading': n,
'value_getter': lambda recs, n1=n: len([r for r in recs if r.reel == n1]), 'value_getter': (lambda recs, n1=n:
'value_getter2': lambda recs, n1=n: time_format(sum([r.time_budget_mins or 0. for r len([r for r in recs if r.reel == n1])
in recs if r.reel == n1])), ),
'style_getter': lambda col_index: [('ALIGN', (col_index, 0), (col_index, -1), 'CENTER'), 'value_getter2': (lambda recs, n1=n:
('LINEAFTER', (col_index, 0), (col_index, -1), .5, colors.gray)], 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 'width': num_column_width
}) })
@@ -104,18 +131,26 @@ def build_columns(lines: List[ADRLine], reel_list: Optional[List[str]], show_pri
for n in range(1, 6,): for n in range(1, 6,):
columns.append({ columns.append({
'heading': 'P%i' % n, 'heading': 'P%i' % n,
'value_getter': lambda recs: len([r for r in recs if r.priority == n]), 'value_getter': lambda recs: len([r for r in recs
'value_getter2': lambda recs: time_format(sum([r.time_budget_mins or 0. if r.priority == n]),
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: [], 'style_getter': lambda col_index: [],
'width': num_column_width 'width': num_column_width
}) })
columns.append({ columns.append({
'heading': '>P5', 'heading': '>P5',
'value_getter': lambda recs: len([r for r in recs if (r.priority or 5) > 5]), 'value_getter': lambda recs: len([r for r in recs
'value_getter2': lambda recs: time_format(sum([r.time_budget_mins or 0. if (r.priority or 5) > 5]),
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: [], 'style_getter': lambda col_index: [],
'width': num_column_width 'width': num_column_width
}) })
@@ -124,32 +159,47 @@ def build_columns(lines: List[ADRLine], reel_list: Optional[List[str]], show_pri
columns.append({ columns.append({
'heading': 'Omit', 'heading': 'Omit',
'value_getter': lambda recs: len([r for r in recs if r.omitted]), '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. 'value_getter2': (lambda recs:
for r in recs if r.omitted])), time_format(sum([r.time_budget_mins or 0.
'style_getter': lambda col_index: [('ALIGN', (col_index, 0), (col_index, -1), 'CENTER')], for r in recs if r.omitted]))),
'style_getter': (lambda col_index:
[('ALIGN', (col_index, 0), (col_index, -1),
'CENTER')]
),
'width': num_column_width 'width': num_column_width
}) })
columns.append({ columns.append({
'heading': 'Total', 'heading': 'Total',
'value_getter': lambda recs: len([r for r in recs if not r.omitted]), '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. 'value_getter2': (lambda recs:
for r in recs if not r.omitted]), zero_str=None), time_format(
'style_getter': lambda col_index: [('LINEBEFORE', (col_index, 0), (col_index, -1), 1.0, colors.black), sum([r.time_budget_mins or 0.
('ALIGN', (col_index, 0), (col_index, -1), 'CENTER')],
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 'width': 0.5 * inch
}) })
return columns return columns
def populate_columns(lines: List[ADRLine], columns, include_omitted, _page_size): def populate_columns(lines: List[ADRLine], columns, include_omitted,
_page_size):
data = list() data = list()
styles = list() styles = list()
columns_widths = list() columns_widths = list()
sorted_character_numbers: List[str] = sorted(set([x.character_id for x in lines]), sorted_character_numbers: List[str] = sorted(
key=lambda x: str(x)) set([x.character_id for x in lines]),
key=lambda x: str(x))
# construct column styles # construct column styles
@@ -174,8 +224,10 @@ def populate_columns(lines: List[ADRLine], columns, include_omitted, _page_size)
row_data.append(col['value_getter'](list(char_records))) row_data.append(col['value_getter'](list(char_records)))
row_data2.append(col['value_getter2'](list(char_records))) row_data2.append(col['value_getter2'](list(char_records)))
styles.extend([('TEXTCOLOR', (0, row2_index), (-1, row2_index), colors.red), styles.extend([('TEXTCOLOR', (0, row2_index), (-1, row2_index),
('LINEBELOW', (0, row2_index), (-1, row2_index), 0.5, colors.black)]) colors.red),
('LINEBELOW', (0, row2_index), (-1, row2_index),
0.5, colors.black)])
data.append(row_data) data.append(row_data)
data.append(row_data2) data.append(row_data2)
@@ -192,7 +244,8 @@ def populate_columns(lines: List[ADRLine], columns, include_omitted, _page_size)
summary_row1.append("") summary_row1.append("")
summary_row2.append("") summary_row2.append("")
styles.append(('LINEABOVE', (0, row1_index), (-1, row1_index), 2.0, colors.black)) styles.append(('LINEABOVE', (0, row1_index), (-1, row1_index), 2.0,
colors.black))
data.append(summary_row1) data.append(summary_row1)
data.append(summary_row2) data.append(summary_row2)
@@ -204,17 +257,20 @@ def populate_columns(lines: List[ADRLine], columns, include_omitted, _page_size)
# pass # pass
def output_report(lines: List[ADRLine], reel_list: List[str], include_omitted=False, def output_report(lines: List[ADRLine], reel_list: List[str],
page_size=portrait(letter), font_name='Helvetica'): include_omitted=False, page_size=portrait(letter),
columns = build_columns(lines, include_omitted=include_omitted, reel_list=reel_list) font_name='Helvetica'):
data, style, columns_widths = populate_columns(lines, columns, include_omitted, page_size) 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(('FONTNAME', (0, 0), (-1, -1), font_name))
style.append(('FONTSIZE', (0, 0), (-1, -1), 9.)) style.append(('FONTSIZE', (0, 0), (-1, -1), 9.))
style.append(('LINEBELOW', (0, 0), (-1, 0), 1.0, colors.black)) style.append(('LINEBELOW', (0, 0), (-1, 0), 1.0, colors.black))
# style.append(('LINEBELOW', (0, 1), (-1, -1), 0.25, colors.gray)) # style.append(('LINEBELOW', (0, 1), (-1, -1), 0.25, colors.gray))
#pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc')) # pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
title = "%s Line Count" % lines[0].title title = "%s Line Count" % lines[0].title
filename = title + '.pdf' filename = title + '.pdf'
@@ -226,7 +282,8 @@ def output_report(lines: List[ADRLine], reel_list: List[str], include_omitted=Fa
document_header='Line Count') document_header='Line Count')
# header_data, header_style, header_widths = build_header(columns_widths) # header_data, header_style, header_widths = build_header(columns_widths)
# header_table = Table(data=header_data, style=header_style, colWidths=header_widths) # header_table = Table(data=header_data, style=header_style,
# colWidths=header_widths)
table = Table(data=data, style=style, colWidths=columns_widths) table = Table(data=data, style=style, colWidths=columns_widths)
@@ -241,6 +298,7 @@ def output_report(lines: List[ADRLine], reel_list: List[str], include_omitted=Fa
omitted_count = len([x for x in lines if x.omitted]) omitted_count = len([x for x in lines if x.omitted])
if not include_omitted and omitted_count > 0: if not include_omitted and omitted_count > 0:
story.append(Paragraph("* %i Omitted lines are excluded." % omitted_count, style)) story.append(Paragraph("* %i Omitted lines are excluded." %
omitted_count, style))
doc.build(story) doc.build(story)

View File

@@ -27,23 +27,28 @@ def build_aux_data_field(line: ADRLine):
tag_field = "" tag_field = ""
if line.effort: if line.effort:
bg_color = 'red' bg_color = 'red'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " % (bg_color, fg_color, "EFF") tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " \
% (bg_color, fg_color, "EFF")
elif line.tv: elif line.tv:
bg_color = 'blue' bg_color = 'blue'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " % (bg_color, fg_color, "TV") tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " \
% (bg_color, fg_color, "TV")
elif line.adlib: elif line.adlib:
bg_color = 'purple' bg_color = 'purple'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " % (bg_color, fg_color, "ADLIB") tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " \
% (bg_color, fg_color, "ADLIB")
elif line.optional: elif line.optional:
bg_color = 'green' bg_color = 'green'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font>" % (bg_color, fg_color, "OPTIONAL") tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font>" \
% (bg_color, fg_color, "OPTIONAL")
entries.append(tag_field) entries.append(tag_field)
return "<br />".join(entries) return "<br />".join(entries)
def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat, font_name='Helvetica'): def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat,
font_name='Helvetica'):
story = list() story = list()
this_scene = None this_scene = None
@@ -60,7 +65,8 @@ def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat, font_name='Helvet
('LEFTPADDING', (0, 0), (0, 0), 0.0), ('LEFTPADDING', (0, 0), (0, 0), 0.0),
('BOTTOMPADDING', (0, 0), (-1, -1), 24.)] ('BOTTOMPADDING', (0, 0), (-1, -1), 24.)]
cue_number_field = "%s<br /><font fontSize=7>%s</font>" % (line.cue_number, line.character_name) cue_number_field = "%s<br /><font fontSize=7>%s</font>" \
% (line.cue_number, line.character_name)
time_data = time_format(line.time_budget_mins) time_data = time_format(line.time_budget_mins)
@@ -79,7 +85,8 @@ def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat, font_name='Helvet
]] ]]
line_table = Table(data=line_table_data, line_table = Table(data=line_table_data,
colWidths=[inch * 0.75, inch, inch * 3., 0.5 * inch, inch * 2.], colWidths=[inch * 0.75, inch, inch * 3., 0.5 * inch,
inch * 2.],
style=table_style) style=table_style)
if (line.scene or "[No Scene]") != this_scene: if (line.scene or "[No Scene]") != this_scene:
@@ -97,7 +104,7 @@ def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat, font_name='Helvet
def build_tc_data(line: ADRLine, tc_format: TimecodeFormat): def build_tc_data(line: ADRLine, tc_format: TimecodeFormat):
tc_data = tc_format.seconds_to_smpte(line.start) + "<br />" + \ tc_data = tc_format.seconds_to_smpte(line.start) + "<br />" + \
tc_format.seconds_to_smpte(line.finish) tc_format.seconds_to_smpte(line.finish)
third_line = [] third_line = []
if line.reel is not None: if line.reel is not None:
if line.reel[0:1] == 'R': if line.reel[0:1] == 'R':
@@ -111,11 +118,12 @@ def build_tc_data(line: ADRLine, tc_format: TimecodeFormat):
return tc_data return tc_data
def generate_report(page_size, lines: List[ADRLine], tc_rate: TimecodeFormat, character_number=None, def generate_report(page_size, lines: List[ADRLine], tc_rate: TimecodeFormat,
include_omitted=True): character_number=None, include_omitted=True):
if character_number is not None: if character_number is not None:
lines = [r for r in lines if r.character_id == character_number] lines = [r for r in lines if r.character_id == character_number]
title = "%s ADR Report (%s)" % (lines[0].title, lines[0].character_name) title = "%s ADR Report (%s)" % (lines[0].title,
lines[0].character_name)
document_header = "%s ADR Report" % lines[0].character_name document_header = "%s ADR Report" % lines[0].character_name
else: else:
title = "%s ADR Report" % lines[0].title title = "%s ADR Report" % lines[0].title

View File

@@ -1,7 +1,7 @@
from reportlab.pdfgen.canvas import Canvas from reportlab.pdfgen.canvas import Canvas
from reportlab.pdfbase import pdfmetrics # from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont # from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.units import inch from reportlab.lib.units import inch
from reportlab.lib.pagesizes import letter from reportlab.lib.pagesizes import letter
@@ -11,20 +11,23 @@ from reportlab.platypus import Paragraph
from .__init__ import GRect from .__init__ import GRect
from ptulsconv.broadcast_timecode import TimecodeFormat, footage_to_frame_count from ptulsconv.broadcast_timecode import TimecodeFormat
from ptulsconv.docparser.adr_entity import ADRLine from ptulsconv.docparser.adr_entity import ADRLine
import datetime import datetime
font_name = 'Helvetica' font_name = 'Helvetica'
def draw_header_block(canvas, rect, record: ADRLine): def draw_header_block(canvas, rect, record: ADRLine):
rect.draw_text_cell(canvas, record.cue_number, "Helvetica", 44, vertical_align='m') rect.draw_text_cell(canvas, record.cue_number, "Helvetica", 44,
vertical_align='m')
def draw_character_row(canvas, rect, record: ADRLine): def draw_character_row(canvas, rect, record: ADRLine):
label_frame, value_frame = rect.split_x(1.25 * inch) label_frame, value_frame = rect.split_x(1.25 * inch)
label_frame.draw_text_cell(canvas, "CHARACTER", font_name, 10, force_baseline=9.) label_frame.draw_text_cell(canvas, "CHARACTER", font_name, 10,
force_baseline=9.)
line = "%s / %s" % (record.character_id, record.character_name) line = "%s / %s" % (record.character_id, record.character_name)
if record.actor_name is not None: if record.actor_name is not None:
line = line + " / " + record.actor_name line = line + " / " + record.actor_name
@@ -33,7 +36,8 @@ def draw_character_row(canvas, rect, record: ADRLine):
def draw_cue_number_block(canvas, rect, record: ADRLine): 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, 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, label_frame.draw_text_cell(canvas, "CUE NUMBER", font_name, 10,
inset_y=5., vertical_align='t') inset_y=5., vertical_align='t')
number_frame.draw_text_cell(canvas, record.cue_number, font_name, 14, number_frame.draw_text_cell(canvas, record.cue_number, font_name, 14,
@@ -55,18 +59,25 @@ def draw_cue_number_block(canvas, rect, record: ADRLine):
rect.draw_border(canvas, 'max_x') rect.draw_border(canvas, 'max_x')
def draw_timecode_block(canvas, rect, record: ADRLine, tc_display_format: TimecodeFormat): 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( (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') [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, in_label_frame.draw_text_cell(canvas, "IN", font_name, 10,
vertical_align='t', inset_y=5., inset_x=5.) 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, in_frame.draw_text_cell(canvas,
inset_x=10., inset_y=2., draw_baseline=True) 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, out_label_frame.draw_text_cell(canvas, "OUT", font_name, 10,
vertical_align='t', inset_y=5., inset_x=5.) 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, out_frame.draw_text_cell(canvas,
inset_x=10., inset_y=2., draw_baseline=True) 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') rect.draw_border(canvas, 'max_x')
@@ -91,13 +102,15 @@ def draw_reason_block(canvas, rect, record: ADRLine):
p = Paragraph(record.note or "", style) p = Paragraph(record.note or "", style)
notes_value.draw_flowable(canvas, p, draw_baselines=True, inset_x=5., inset_y=5.) notes_value.draw_flowable(canvas, p, draw_baselines=True,
inset_x=5., inset_y=5.)
def draw_prompt(canvas, rect, prompt=""): def draw_prompt(canvas, rect, prompt=""):
label, block = rect.split_y(0.20 * inch, direction='d') 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.) label.draw_text_cell(canvas, "PROMPT", font_name, 10, vertical_align='t',
inset_y=5., inset_x=0.)
style = getSampleStyleSheet()['BodyText'] style = getSampleStyleSheet()['BodyText']
style.fontName = font_name style.fontName = font_name
@@ -117,7 +130,8 @@ def draw_prompt(canvas, rect, prompt=""):
def draw_notes(canvas, rect, note=""): def draw_notes(canvas, rect, note=""):
label, block = rect.split_y(0.20 * inch, direction='d') 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.) label.draw_text_cell(canvas, "NOTES", font_name, 10, vertical_align='t',
inset_y=5., inset_x=0.)
style = getSampleStyleSheet()['BodyText'] style = getSampleStyleSheet()['BodyText']
style.fontName = font_name style.fontName = font_name
@@ -169,31 +183,43 @@ def draw_take_grid(canvas, rect):
canvas.restoreState() canvas.restoreState()
def draw_aux_block(canvas, rect, recording_time_sec_this_line, recording_time_sec): def draw_aux_block(canvas, rect, recording_time_sec_this_line,
recording_time_sec):
rect.draw_border(canvas, 'min_x') rect.draw_border(canvas, 'min_x')
content_rect = rect.inset_xy(10., 10.) content_rect = rect.inset_xy(10., 10.)
lines, last_line = content_rect.divide_y([12., 12., 24., 24., 24., 24.], direction='d') lines, last_line = content_rect.divide_y([12., 12., 24., 24., 24., 24.],
direction='d')
lines[0].draw_text_cell(canvas, lines[0].draw_text_cell(canvas,
"Time for this line: %.1f mins" % (recording_time_sec_this_line / 60.), font_name, 9.) "Time for this line: %.1f mins" %
lines[1].draw_text_cell(canvas, "Running time: %03.1f mins" % (recording_time_sec / 60.), font_name, 9.) (recording_time_sec_this_line / 60.),
lines[2].draw_text_cell(canvas, "Actual Start: ______________", font_name, 9., vertical_align='b') font_name, 9.)
lines[3].draw_text_cell(canvas, "Record Date: ______________", font_name, 9., vertical_align='b') lines[1].draw_text_cell(canvas, "Running time: %03.1f mins" %
lines[4].draw_text_cell(canvas, "Engineer: ______________", font_name, 9., vertical_align='b') (recording_time_sec / 60.), font_name, 9.)
lines[5].draw_text_cell(canvas, "Location: ______________", font_name, 9., vertical_align='b') 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): def draw_footer(canvas, rect, record: ADRLine, report_date, line_no,
total_lines):
rect.draw_border(canvas, 'max_y') rect.draw_border(canvas, 'max_y')
report_date_s = [report_date.strftime("%c")] report_date_s = [report_date.strftime("%c")]
spotting_name = [record.spot] if record.spot is not None else [] spotting_name = [record.spot] if record.spot is not None else []
pages_s = ["Line %i of %i" % (line_no, total_lines)] pages_s = ["Line %i of %i" % (line_no, total_lines)]
footer_s = " - ".join(report_date_s + spotting_name + pages_s) 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.) 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): def create_report_for_character(records, report_date,
tc_display_format: TimecodeFormat):
outfile = "%s_%s_%s_Log.pdf" % (records[0].title, outfile = "%s_%s_%s_Log.pdf" % (records[0].title,
records[0].character_id, records[0].character_id,
@@ -201,20 +227,24 @@ def create_report_for_character(records, report_date, tc_display_format: Timecod
assert outfile is not None assert outfile is not None
assert outfile[-4:] == '.pdf', "Output file must have 'pdf' extension!" assert outfile[-4:] == '.pdf', "Output file must have 'pdf' extension!"
#pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc')) # pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
page: GRect = GRect(0, 0, letter[0], letter[1]) page: GRect = GRect(0, 0, letter[0], letter[1])
page = page.inset(inch * 0.5) page = page.inset(inch * 0.5)
(header_row, char_row, data_row, prompt_row, notes_row, takes_row), footer = \ (header_row, char_row, data_row,
page.divide_y([0.875 * inch, 0.375 * inch, inch, 3.0 * inch, 1.5 * inch, 3 * inch], direction='d') 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_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]) (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) (take_grid_block), aux_block = takes_row.split_x(5.25 * inch)
c = Canvas(outfile, pagesize=letter,) c = Canvas(outfile, pagesize=letter,)
c.setTitle("%s %s (%s) Supervisor's Log" % (records[0].title, records[0].character_name, c.setTitle("%s %s (%s) Supervisor's Log" % (records[0].title,
records[0].character_name,
records[0].character_id)) records[0].character_id))
c.setAuthor(records[0].supervisor) c.setAuthor(records[0].supervisor)
@@ -223,7 +253,8 @@ def create_report_for_character(records, report_date, tc_display_format: Timecod
line_n = 1 line_n = 1
for record in records: for record in records:
record: ADRLine record: ADRLine
recording_time_sec_this_line: float = (record.time_budget_mins or 6.0) * 60.0 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 recording_time_sec = recording_time_sec + recording_time_sec_this_line
draw_header_block(c, cue_header_block, record) draw_header_block(c, cue_header_block, record)
@@ -233,14 +264,17 @@ def create_report_for_character(records, report_date, tc_display_format: Timecod
# draw_title_box(c, title_header_block, record) # draw_title_box(c, title_header_block, record)
draw_character_row(c, char_row, record) draw_character_row(c, char_row, record)
draw_cue_number_block(c, cue_number_block, record) draw_cue_number_block(c, cue_number_block, record)
draw_timecode_block(c, timecode_block, record, tc_display_format=tc_display_format) draw_timecode_block(c, timecode_block, record,
tc_display_format=tc_display_format)
draw_reason_block(c, reason_block, record) draw_reason_block(c, reason_block, record)
draw_prompt(c, prompt_row, prompt=record.prompt) draw_prompt(c, prompt_row, prompt=record.prompt or "")
draw_notes(c, notes_row, note="") draw_notes(c, notes_row, note="")
draw_take_grid(c, take_grid_block) draw_take_grid(c, take_grid_block)
draw_aux_block(c, aux_block, recording_time_sec_this_line, recording_time_sec) 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) draw_footer(c, footer, record, report_date, line_no=line_n,
total_lines=total_lines)
line_n = line_n + 1 line_n = line_n + 1
c.showPage() c.showPage()
@@ -254,5 +288,6 @@ def output_report(lines, tc_display_format: TimecodeFormat):
character_numbers = set([x.character_id for x in lines]) character_numbers = set([x.character_id for x in lines])
for n in character_numbers: for n in character_numbers:
create_report_for_character([e for e in events if e.character_id == n], report_date, create_report_for_character([e for e in events if e.character_id == n],
report_date,
tc_display_format=tc_display_format) tc_display_format=tc_display_format)

View File

@@ -5,36 +5,42 @@ from .__init__ import make_doc_template
from reportlab.lib.units import inch from reportlab.lib.units import inch
from reportlab.lib.pagesizes import letter from reportlab.lib.pagesizes import letter
from reportlab.platypus import Paragraph, Spacer, KeepTogether, Table, HRFlowable from reportlab.platypus import Paragraph, Spacer, KeepTogether, Table, \
HRFlowable
from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors from reportlab.lib import colors
from reportlab.pdfbase import pdfmetrics # from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont # from reportlab.pdfbase.ttfonts import TTFont
from ..broadcast_timecode import TimecodeFormat from ..broadcast_timecode import TimecodeFormat
from ..docparser.adr_entity import ADRLine from ..docparser.adr_entity import ADRLine
def output_report(lines: List[ADRLine], tc_display_format: TimecodeFormat, font_name="Helvetica"): def output_report(lines: List[ADRLine], tc_display_format: TimecodeFormat,
font_name="Helvetica"):
character_numbers = set([n.character_id for n in lines]) character_numbers = set([n.character_id for n in lines])
#pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc')) # pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
for n in character_numbers: for n in character_numbers:
char_lines = [line for line in lines if not line.omitted and line.character_id == n] char_lines = [line for line in lines
if not line.omitted and line.character_id == n]
character_name = char_lines[0].character_name character_name = char_lines[0].character_name
char_lines = sorted(char_lines, key=lambda line: line.start) char_lines = sorted(char_lines, key=lambda line: line.start)
title = "%s (%s) %s ADR Script" % (char_lines[0].title, character_name, n) title = "%s (%s) %s ADR Script" % (char_lines[0].title,
filename = "%s_%s_%s_ADR Script.pdf" % (char_lines[0].title, n, character_name) 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, doc = make_doc_template(page_size=letter, filename=filename,
document_title=title,
title=char_lines[0].title, title=char_lines[0].title,
document_subheader=char_lines[0].spot, document_subheader=char_lines[0].spot or "",
supervisor=char_lines[0].supervisor, supervisor=char_lines[0].supervisor or "",
client=char_lines[0].client, client=char_lines[0].client or "",
document_header=character_name) document_header=character_name or "")
story = [] story = []
@@ -58,7 +64,8 @@ def output_report(lines: List[ADRLine], tc_display_format: TimecodeFormat, font_
start_tc = tc_display_format.seconds_to_smpte(line.start) start_tc = tc_display_format.seconds_to_smpte(line.start)
finish_tc = tc_display_format.seconds_to_smpte(line.finish) finish_tc = tc_display_format.seconds_to_smpte(line.finish)
data_block = [[Paragraph(line.cue_number, number_style), data_block = [[Paragraph(line.cue_number, number_style),
Paragraph(start_tc + " - " + finish_tc, number_style) Paragraph(start_tc + " - " + finish_tc,
number_style)
]] ]]
# RIGHTWARDS ARROW → # RIGHTWARDS ARROW →

View File

@@ -35,13 +35,15 @@ def print_warning(warning_string):
sys.stderr.write(" - %s\n" % warning_string) sys.stderr.write(" - %s\n" % warning_string)
def print_advisory_tagging_error(failed_string, position, parent_track_name=None, clip_time=None): def print_advisory_tagging_error(failed_string, position,
parent_track_name=None, clip_time=None):
if sys.stderr.isatty(): if sys.stderr.isatty():
sys.stderr.write("\n") sys.stderr.write("\n")
sys.stderr.write(" ! \033[33;1mTagging error: \033[0m") sys.stderr.write(" ! \033[33;1mTagging error: \033[0m")
ok_string = failed_string[:position] ok_string = failed_string[:position]
not_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)) 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: if parent_track_name is not None:
sys.stderr.write(" ! > On track \"%s\"\n" % parent_track_name) sys.stderr.write(" ! > On track \"%s\"\n" % parent_track_name)

View File

@@ -14,15 +14,20 @@ class ValidationError:
def report_message(self): def report_message(self):
if self.event is not None: if self.event is not None:
return f"{self.message}: event at {self.event.start} with number {self.event.cue_number}" return (f"{self.message}: event at {self.event.start} with number"
"{self.event.cue_number}")
else: else:
return self.message return self.message
def validate_unique_count(input_lines: Iterator[ADRLine], field='title', count=1): def validate_unique_count(input_lines: Iterator[ADRLine], field='title',
count=1):
values = set(list(map(lambda e: getattr(e, field), input_lines))) values = set(list(map(lambda e: getattr(e, field), input_lines)))
if len(values) > count: if len(values) > count:
yield ValidationError(message="Field {} has too many values (max={}): {}".format(field, count, values)) yield ValidationError(
message="Field {} has too many values (max={}): {}"
.format(field, count, values)
)
def validate_value(input_lines: Iterator[ADRLine], key_field, predicate): def validate_value(input_lines: Iterator[ADRLine], key_field, predicate):
@@ -33,7 +38,8 @@ def validate_value(input_lines: Iterator[ADRLine], key_field, predicate):
event=event) event=event)
def validate_unique_field(input_lines: Iterator[ADRLine], field='cue_number', scope=None): def validate_unique_field(input_lines: Iterator[ADRLine], field='cue_number',
scope=None):
values = dict() values = dict()
for event in input_lines: for event in input_lines:
this = getattr(event, field) this = getattr(event, field)
@@ -44,26 +50,31 @@ def validate_unique_field(input_lines: Iterator[ADRLine], field='cue_number', sc
values.setdefault(key, set()) values.setdefault(key, set())
if this in values[key]: if this in values[key]:
yield ValidationError(message='Re-used {}'.format(field), event=event) yield ValidationError(message='Re-used {}'.format(field),
event=event)
else: else:
values[key].update(this) values[key].update(this)
def validate_non_empty_field(input_lines: Iterator[ADRLine], field='cue_number'): def validate_non_empty_field(input_lines: Iterator[ADRLine],
field='cue_number'):
for event in input_lines: for event in input_lines:
if getattr(event, field, None) is None: if getattr(event, field, None) is None:
yield ValidationError(message='Empty field {}'.format(field), event=event) yield ValidationError(message='Empty field {}'.format(field),
event=event)
def validate_dependent_value(input_lines: Iterator[ADRLine], key_field, dependent_field): 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 Validates that two events with the same value in `key_field` always have
same value in `dependent_field` the same value in `dependent_field`
""" """
key_values = set((getattr(x, key_field) for x in input_lines)) key_values = set((getattr(x, key_field) for x in input_lines))
for key_value in key_values: for key_value in key_values:
rows = [(getattr(x, key_field), getattr(x, dependent_field)) for x in input_lines rows = [(getattr(x, key_field), getattr(x, dependent_field))
for x in input_lines
if getattr(x, key_field) == key_value] if getattr(x, key_field) == key_value]
unique_rows = set(rows) unique_rows = set(rows)
if len(unique_rows) > 1: if len(unique_rows) > 1:

View File

@@ -12,7 +12,10 @@ import ptulsconv
from ptulsconv.docparser.adr_entity import ADRLine from ptulsconv.docparser.adr_entity import ADRLine
# TODO Get a third-party test for Avid Marker lists # 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):
def avid_marker_list(lines: List[ADRLine], report_date=datetime.datetime.now(),
reel_start_frame=0, fps=24):
doc = TreeBuilder(element_factory=None) doc = TreeBuilder(element_factory=None)
doc.start('Avid:StreamItems', {'xmlns:Avid': 'http://www.avid.com'}) doc.start('Avid:StreamItems', {'xmlns:Avid': 'http://www.avid.com'})
@@ -48,26 +51,35 @@ def avid_marker_list(lines: List[ADRLine], report_date=datetime.datetime.now(),
for line in lines: for line in lines:
doc.start('AvClass', {'id': 'ATTR'}) doc.start('AvClass', {'id': 'ATTR'})
doc.start('AvProp', {'id': 'ATTR', 'name': '__OMFI:ATTR:NumItems', 'type': 'int32'}) doc.start('AvProp', {'id': 'ATTR',
'name': '__OMFI:ATTR:NumItems',
'type': 'int32'})
doc.data('7') doc.data('7')
doc.end('AvProp') doc.end('AvProp')
doc.start('List', {'id': 'OMFI:ATTR:AttrRefs'}) 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('1', 'OMFI:ATTB:IntAttribute', 'int32',
insert_elem('2', 'OMFI:ATTB:StringAttribute', 'string', '_ATN_CRM_COLOR', 'yellow') '_ATN_CRM_LONG_CREATE_DATE', report_date.strftime("%s"))
insert_elem('2', 'OMFI:ATTB:StringAttribute', 'string', '_ATN_CRM_USER', line.supervisor or "") 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) marker_name = "%s: %s" % (line.cue_number, line.prompt)
insert_elem('2', 'OMFI:ATTB:StringAttribute', 'string', '_ATN_CRM_COM', marker_name) insert_elem('2', 'OMFI:ATTB:StringAttribute', 'string',
'_ATN_CRM_COM', marker_name)
start_frame = int(line.start * fps) start_frame = int(line.start * fps)
insert_elem('2', "OMFI:ATTB:StringAttribute", 'string', '_ATN_CRM_TC', insert_elem('2', "OMFI:ATTB:StringAttribute", 'string',
'_ATN_CRM_TC',
str(start_frame - reel_start_frame)) str(start_frame - reel_start_frame))
insert_elem('2', "OMFI:ATTB:StringAttribute", 'string', '_ATN_CRM_TRK', 'V1') insert_elem('2', "OMFI:ATTB:StringAttribute", 'string',
insert_elem('1', "OMFI:ATTB:IntAttribute", 'int32', '_ATN_CRM_LENGTH', '1') '_ATN_CRM_TRK', 'V1')
insert_elem('1', "OMFI:ATTB:IntAttribute", 'int32',
'_ATN_CRM_LENGTH', '1')
doc.start('ListElem', {}) doc.start('ListElem', {})
doc.end('ListElem') doc.end('ListElem')
@@ -82,17 +94,22 @@ def avid_marker_list(lines: List[ADRLine], report_date=datetime.datetime.now(),
def dump_fmpxml(data, input_file_name, output, adr_field_map): def dump_fmpxml(data, input_file_name, output, adr_field_map):
doc = TreeBuilder(element_factory=None) doc = TreeBuilder(element_factory=None)
doc.start('FMPXMLRESULT', {'xmlns': 'http://www.filemaker.com/fmpxmlresult'}) doc.start('FMPXMLRESULT', {'xmlns':
'http://www.filemaker.com/fmpxmlresult'})
doc.start('ERRORCODE', {}) doc.start('ERRORCODE', {})
doc.data('0') doc.data('0')
doc.end('ERRORCODE') doc.end('ERRORCODE')
doc.start('PRODUCT', {'NAME': ptulsconv.__name__, 'VERSION': ptulsconv.__version__}) doc.start('PRODUCT', {'NAME': ptulsconv.__name__,
'VERSION': ptulsconv.__version__})
doc.end('PRODUCT') doc.end('PRODUCT')
doc.start('DATABASE', {'DATEFORMAT': 'MM/dd/yy', 'LAYOUT': 'summary', 'TIMEFORMAT': 'hh:mm:ss', doc.start('DATABASE', {'DATEFORMAT': 'MM/dd/yy',
'RECORDS': str(len(data['events'])), 'NAME': os.path.basename(input_file_name)}) 'LAYOUT': 'summary',
'TIMEFORMAT': 'hh:mm:ss',
'RECORDS': str(len(data['events'])),
'NAME': os.path.basename(input_file_name)})
doc.end('DATABASE') doc.end('DATABASE')
doc.start('METADATA', {}) doc.start('METADATA', {})
@@ -102,7 +119,8 @@ def dump_fmpxml(data, input_file_name, output, adr_field_map):
if tp is int or tp is float: if tp is int or tp is float:
ft = 'NUMBER' ft = 'NUMBER'
doc.start('FIELD', {'EMPTYOK': 'YES', 'MAXREPEAT': '1', 'NAME': field[1], 'TYPE': ft}) doc.start('FIELD', {'EMPTYOK': 'YES', 'MAXREPEAT': '1',
'NAME': field[1], 'TYPE': ft})
doc.end('FIELD') doc.end('FIELD')
doc.end('METADATA') doc.end('METADATA')
@@ -157,7 +175,8 @@ def fmp_transformed_dump(data, input_file, xsl_name, output, adr_field_map):
print_status_style("Running xsltproc") print_status_style("Running xsltproc")
xsl_path = os.path.join(pathlib.Path(__file__).parent.absolute(), 'xslt', xsl_name + ".xsl") xsl_path = os.path.join(pathlib.Path(__file__).parent.absolute(), 'xslt',
xsl_name + ".xsl")
print_status_style("Using xsl: %s" % xsl_path) print_status_style("Using xsl: %s" % xsl_path)
subprocess.run(['xsltproc', xsl_path, '-'], subprocess.run(['xsltproc', xsl_path, '-'],
input=str_data, text=True, input=str_data, text=True,

View File

@@ -1,48 +1,52 @@
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"
[project] [project]
name = "ptulsconv" name = "ptulsconv"
authors = [
{name = "Jamie Hardt", email = "jamiehardt@me.com"},
]
readme = "README.md"
license = { file = "LICENSE" } license = { file = "LICENSE" }
classifiers = [ classifiers = [
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Topic :: Multimedia', 'Topic :: Multimedia',
'Topic :: Multimedia :: Sound/Audio', 'Topic :: Multimedia :: Sound/Audio',
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Topic :: Text Processing :: Filters" "Topic :: Text Processing :: Filters"
] ]
requires-python = ">=3.7" requires-python = ">=3.8"
dynamic = ["version", "description"]
keywords = ["text-processing", "parsers", "film", keywords = ["text-processing", "parsers", "film",
"broadcast", "editing", "editorial"] "broadcast", "editing", "editorial"]
dependencies = ['parsimonious', 'tqdm', 'reportlab']
[project.optional-dependencies] [tool.poetry]
doc = [
"Sphinx ~= 5.3.0",
"sphinx-rtd-theme >= 1.1.1"
]
[tool.flit.module]
name = "ptulsconv" name = "ptulsconv"
version = "2.2.4"
description = "Read Pro Tools Text exports and generate PDF ADR Reports, JSON"
authors = ["Jamie Hardt <jamiehardt@me.com>"]
license = "MIT"
readme = "README.md"
[project.scripts] [tool.poetry.dependencies]
ptulsconv = "ptulsconv.__main__:main" 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}
[project.entry_points.console_scripts] [tool.poetry.extras]
doc = ['sphinx', 'sphinx_rtd_theme']
[tool.poetry.scripts]
ptulsconv = 'ptulsconv.__main__:main' ptulsconv = 'ptulsconv.__main__:main'
[project.urls] [project.urls]
Source = 'https://github.com/iluvcapra/ptulsconv' Source = 'https://github.com/iluvcapra/ptulsconv'
Issues = 'https://github.com/iluvcapra/ptulsconv/issues' Issues = 'https://github.com/iluvcapra/ptulsconv/issues'
Documentation = 'https://ptulsconv.readthedocs.io/' Documentation = 'https://ptulsconv.readthedocs.io/'
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -0,0 +1,24 @@
SESSION NAME: Test for ptulsconv
SAMPLE RATE: 48000.000000
BIT DEPTH: 24-bit
SESSION START TIMECODE: 00:00:00:00
TIMECODE FORMAT: 23.976 Frame
# OF AUDIO TRACKS: 1
# OF AUDIO CLIPS: 0
# OF AUDIO FILES: 0
T R A C K L I S T I N G
TRACK NAME: Hamlet
COMMENTS: {Actor=Laurence Olivier}
USER DELAY: 0 Samples
STATE:
CHANNEL EVENT CLIP NAME START TIME END TIME DURATION STATE
1 1 Test Line 1 $QN=T1001 00:00:00:00 00:00:02:00 00:00:02:00 Unmuted
1 2 Test Line 2 $QN=T1002 00:00:04:00 00:00:06:00 00:00:02:00 Unmuted
M A R K E R S L I S T I N G
# LOCATION TIME REFERENCE UNITS NAME TRACK NAME TRACK TYPE COMMENTS
1 00:00:00:00 0 Samples {Title=Multiple Marker Rulers Project} Markers Ruler
2 00:00:04:00 192192 Samples Track Marker Hamlet Track

View File

@@ -7,8 +7,8 @@ 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): def test_header_export(self):
with open(self.path,"r") as file:
session = parse_document(self.path) session = parse_document(file.read())
self.assertIsNotNone(session.header) self.assertIsNotNone(session.header)
self.assertEqual(session.header.session_name, 'Robin Hood Spotting') self.assertEqual(session.header.session_name, 'Robin Hood Spotting')
@@ -19,7 +19,8 @@ class TestRobinHood1(unittest.TestCase):
def test_all_sections(self): def test_all_sections(self):
session = parse_document(self.path) with open(self.path,"r") as file:
session = parse_document(file.read())
self.assertIsNotNone(session.header) self.assertIsNotNone(session.header)
self.assertIsNotNone(session.files) self.assertIsNotNone(session.files)
@@ -30,7 +31,8 @@ class TestRobinHood1(unittest.TestCase):
def test_tracks(self): def test_tracks(self):
session = parse_document(self.path) with open(self.path,"r") as file:
session = parse_document(file.read())
self.assertEqual(len(session.tracks), 14) self.assertEqual(len(session.tracks), 14)
self.assertListEqual(["Scenes", "Robin", "Will", "Marian", "John", self.assertListEqual(["Scenes", "Robin", "Will", "Marian", "John",
@@ -54,7 +56,10 @@ class TestRobinHood1(unittest.TestCase):
list(map(lambda t: t.comments, session.tracks))) list(map(lambda t: t.comments, session.tracks)))
def test_a_track(self): def test_a_track(self):
session = parse_document(self.path)
with open(self.path,"r") as file:
session = parse_document(file.read())
guy_track = session.tracks[5] guy_track = session.tracks[5]
self.assertEqual(guy_track.name, 'Guy') self.assertEqual(guy_track.name, 'Guy')
self.assertEqual(guy_track.comments, '[ADR] {Actor=Basil Rathbone} $CN=5') self.assertEqual(guy_track.comments, '[ADR] {Actor=Basil Rathbone} $CN=5')
@@ -71,7 +76,8 @@ class TestRobinHood1(unittest.TestCase):
self.assertEqual(guy_track.clips[5].state, 'Unmuted') self.assertEqual(guy_track.clips[5].state, 'Unmuted')
def test_memory_locations(self): def test_memory_locations(self):
session = parse_document(self.path) with open(self.path,"r") as file:
session = parse_document(file.read())
self.assertEqual(len(session.markers), 1) self.assertEqual(len(session.markers), 1)
self.assertEqual(session.markers[0].number, 1) self.assertEqual(session.markers[0].number, 1)

View File

@@ -7,23 +7,30 @@ 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): def test_skipped_segments(self):
session = parse_document(self.path) with open(self.path,"r") as file:
session = parse_document(file.read())
self.assertIsNone(session.files) self.assertIsNone(session.files)
self.assertIsNone(session.clips) self.assertIsNone(session.clips)
def test_plugins(self): def test_plugins(self):
session = parse_document(self.path) with open(self.path,"r") as file:
session = parse_document(file.read())
self.assertEqual(len(session.plugins), 2) self.assertEqual(len(session.plugins), 2)
def test_stereo_track(self): def test_stereo_track(self):
session = parse_document(self.path) 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(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[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[1].clip_name, 'RobinHood.1-01.R')
def test_a_track(self): def test_a_track(self):
session = parse_document(self.path) with open(self.path,"r") as file:
session = parse_document(file.read())
guy_track = session.tracks[8] guy_track = session.tracks[8]
self.assertEqual(guy_track.name, 'Guy') self.assertEqual(guy_track.name, 'Guy')

View File

@@ -7,7 +7,9 @@ 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): def test_a_track(self):
session = parse_document(self.path) with open(self.path, "r") as file:
session = parse_document(file.read())
marian_track = session.tracks[6] marian_track = session.tracks[6]
self.assertEqual(marian_track.name, 'Marian') self.assertEqual(marian_track.name, 'Marian')

View File

@@ -7,11 +7,16 @@ 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): def test_header_export_df(self):
session = parse_document(self.path)
with open(self.path, "r") as file:
session = parse_document(file.read())
self.assertEqual(session.header.timecode_drop_frame, True) self.assertEqual(session.header.timecode_drop_frame, True)
def test_a_track(self): def test_a_track(self):
session = parse_document(self.path)
with open(self.path, "r") as file:
session = parse_document(file.read())
guy_track = session.tracks[4] guy_track = session.tracks[4]
self.assertEqual(guy_track.name, 'Robin') self.assertEqual(guy_track.name, 'Robin')

View File

@@ -2,33 +2,52 @@ import unittest
import tempfile import tempfile
import sys
import os.path import os.path
import os import os
import glob import glob
from ptulsconv import commands from ptulsconv import commands
class TestPDFExport(unittest.TestCase): class TestPDFExport(unittest.TestCase):
def test_report_generation(self): def test_report_generation(self):
""" """
Setp through every text file in export_cases and make sure it can Setp through every text file in export_cases and make sure it can
be converted into PDF docs without throwing an error be converted into PDF docs without throwing an error
""" """
files = [os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting.txt"] files = []
#files.append(os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting2.txt") files = [os.path.dirname(__file__) +
"/../export_cases/Robin Hood Spotting.txt"]
for path in files: for path in files:
tempdir = tempfile.TemporaryDirectory() tempdir = tempfile.TemporaryDirectory()
os.chdir(tempdir.name) os.chdir(tempdir.name)
try: try:
commands.convert(path, major_mode='doc') commands.convert(input_file=path, major_mode='doc')
except: except Exception as e:
assert False, "Error processing file %s" % path print("Error in test_report_generation")
print(f"File: {path}")
print(repr(e))
raise e
finally:
tempdir.cleanup()
def test_report_generation_track_markers(self):
files = []
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')
except Exception as e:
print("Error in test_report_generation_track_markers")
print(f"File: {path}")
print(repr(e))
raise e
finally: finally:
tempdir.cleanup() tempdir.cleanup()
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -88,7 +88,9 @@ class TestTagCompiler(unittest.TestCase):
state='Unmuted', state='Unmuted',
timestamp=None), timestamp=None),
] ]
test_track = doc_entity.TrackDescriptor(name="Track 1 [A] {Color=Red} $Mode=1", test_track = doc_entity.TrackDescriptor(
index=0,
name="Track 1 [A] {Color=Red} $Mode=1",
comments="{Comment=This is some text in the comments}", comments="{Comment=This is some text in the comments}",
user_delay_samples=0, user_delay_samples=0,
plugins=[], plugins=[],
@@ -100,14 +102,14 @@ class TestTagCompiler(unittest.TestCase):
time_reference=48000 * 3600, time_reference=48000 * 3600,
units="Samples", units="Samples",
name="Marker 1 {Part=1}", name="Marker 1 {Part=1}",
comments="" comments="", track_marker=False,
), ),
doc_entity.MarkerDescriptor(number=2, doc_entity.MarkerDescriptor(number=2,
location="01:00:01:00", location="01:00:01:00",
time_reference=48000 * 3601, time_reference=48000 * 3601,
units="Samples", units="Samples",
name="Marker 2 {Part=2}", name="Marker 2 {Part=2}",
comments="[M1]" comments="[M1]", track_marker=False,
), ),
] ]