Compare commits

..

97 Commits

Author SHA1 Message Date
jamie ab19793bd1 Merge branch 'master' of https://github.com/iluvcapra/ptulsconv into bug-ruff-2026 2026-09-17 20:41:35 -07:00
jamie 7730f6be23 Ruff fixes in progress 2026-09-17 20:35:11 -07:00
jamie e286132101 Ruff fixes in progress 2026-09-17 20:26:12 -07:00
jamie 4afe4b7a3e Ruff fixes in progress 2026-09-17 20:25:28 -07:00
jamie cb8de2d785 Ruff fixes in progress 2026-09-17 20:20:54 -07:00
jamie 9e9ad75f77 Ruff fixes in progress 2026-09-17 20:06:46 -07:00
jamie f7808e2236 Ruff format 2026-09-17 15:23:42 -07:00
Jamie Hardt 90f78c8fa2 Update CONTRIBUTING.md
Updating Agents policy
2026-05-03 12:16:07 -07:00
Jamie Hardt ebd0b54b6a Update .readthedocs.yaml 2026-03-30 11:54:59 -07:00
Jamie Hardt 98958c42f4 Update .readthedocs.yaml 2026-03-30 11:50:00 -07:00
Jamie Hardt bf9ff7b00f Documentation updates (#18)
* Update .readthedocs.yaml

Updated RTD version to `ubuntu-lts-latest`

* Update .readthedocs.yaml

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

* Updated Flake8 settings

* Updating github workflow

* Update python-package.yml

Removed redundant flake8 run

* Twiddle

* Version number in banner

* Changing to ruff linter

* Tweaking workflow

* Update python-package.yml

* Update __init__.py

* Update __init__.py

* removing flake8 file

* Doc notes

* Twiddling with RtD

---------

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

* Updated Flake8 settings

* Updating github workflow

* Update python-package.yml

* Removed redundant flake8 run

* Version number in banner

---------

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