mirror of
https://github.com/iluvcapra/ptulsconv.git
synced 2026-10-02 01:37:17 +00:00
Compare commits
122 Commits
v1.0.6
...
9349f4ba1e
| Author | SHA1 | Date | |
|---|---|---|---|
| 9349f4ba1e | |||
| e0e3024280 | |||
| 2fc8a653cd | |||
| 183a7ef687 | |||
| e0fe946455 | |||
| 12ecb86bdf | |||
| 6f2367c9fd | |||
| 8a23d7c702 | |||
| 90f78c8fa2 | |||
| ebd0b54b6a | |||
| 98958c42f4 | |||
| bf9ff7b00f | |||
| c45ca3a8fd | |||
| 1fd86a6d7d | |||
| e593996b65 | |||
| 32191791c6 | |||
| 1ff910e4d2 | |||
| 0e8a77f548 | |||
| 7058397f6f | |||
| 16d7befd9f | |||
| 2a3f4a7c18 | |||
| d7fff23e5e | |||
| 757c3171e7 | |||
| 48dd7de07e | |||
| dc259d54aa | |||
| 09ed12fc8f | |||
| 2a69bf47c8 | |||
| 01d2374a9b | |||
| 68e7eb89a9 | |||
| 517fe3526a | |||
| 7789c0df4c | |||
| d97dcc3e08 | |||
| 615b2b5a2a | |||
| 4cd6ba1772 | |||
| 1942b323b3 | |||
| 7d297a7564 | |||
| 54fa8f04a7 | |||
| dcc6113a63 | |||
| 04b9e35240 | |||
| f460022160 | |||
| 5c5cd84811 | |||
| ee90697be0 | |||
| 4a0d19ade1 | |||
| df6c783c51 | |||
| f0b232b2b6 | |||
| 519c6403ba | |||
| d29c36eafa | |||
| 2095a1fb75 | |||
| 70defcc46c | |||
| d156b6df89 | |||
| 3ba9d7933e | |||
| b0c40ee0b6 | |||
| 921b0f07af | |||
| 57764bc859 | |||
| 779c93282c | |||
| 9684be6c7e | |||
| 484a70fc8e | |||
| 5aa005c317 | |||
| 454adea3d1 | |||
| 1e6546dab5 | |||
| 8b262d3bfb | |||
| 630e7960dc | |||
| aa7b418121 | |||
| a519a525b2 | |||
| 1412efe509 | |||
| 12a6c05467 | |||
| cf87986014 | |||
| 67533879f8 | |||
| f847b88aa3 | |||
| c3a600c5d7 | |||
| 914783a809 | |||
| c638c673e8 | |||
| 15fe6667af | |||
| d4e23b59eb | |||
| a602b09551 | |||
| 448d93d717 | |||
| 59e7d40d97 | |||
| eaa5fe824f | |||
| 8ebfd32e02 | |||
| 83a9adb48a | |||
| 013ebcbe75 | |||
| c87695e5fe | |||
| 4a8983cbbb | |||
| 9123cbd0b5 | |||
| 4224d106b0 | |||
| ac22fab97f | |||
| 64ca2c6c5c | |||
| c3af30dc6a | |||
| c30f675cec | |||
| 204af7d9cb | |||
| 10fc211e80 | |||
| d56c7df376 | |||
| 7b38449a5f | |||
| 17b87b6e69 | |||
| a636791539 | |||
| dfde3c4493 | |||
| 81909c8a51 | |||
| e2b9a20870 | |||
| 006cec05e5 | |||
| a95f0b5cca | |||
| 70a5206d73 | |||
| 128eed002d | |||
| f8a0d70942 | |||
| 5f29e95ba9 | |||
| 82f07b13a6 | |||
| fbcbba1098 | |||
| 622f04963f | |||
| 5b36dcb5aa | |||
| fd02d962d0 | |||
| 2021159666 | |||
| f825b92586 | |||
| 4318946596 | |||
| 2a98954885 | |||
| 79d8cc5b69 | |||
| 5785dc3364 | |||
| 4e64edcd85 | |||
| 58277367c5 | |||
| 617f34a515 | |||
| 5427b4cfb1 | |||
| 408829e820 | |||
| b65401d25f | |||
| 50fe3e2c0a |
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.5.0
|
||||
@@ -27,14 +27,11 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8 pytest
|
||||
pip install -e .
|
||||
- name: Lint with flake8
|
||||
pip install .
|
||||
pip install --group dev .
|
||||
- name: Lint with ruff
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 ptulsconv tests --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 ptulsconv tests --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
ruff check src
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest
|
||||
|
||||
@@ -4,36 +4,26 @@ on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2.5.0
|
||||
- uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4.3.0
|
||||
uses: actions/setup-python@v4.6.0
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build twine
|
||||
- name: Install parsimonious
|
||||
run: |
|
||||
pip install parsimonious
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_UPLOAD_API_KEY }}
|
||||
run: |
|
||||
python -m build
|
||||
twine upload dist/*
|
||||
- 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 }}
|
||||
pip install build
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
- name: pypi-publish
|
||||
uses: pypa/gh-action-pypi-publish@v1.13.0
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Test Toot
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
print-tag:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Report to Mastodon
|
||||
uses: cbrgm/mastodon-github-action@v1.0.1
|
||||
env:
|
||||
MASTODON_URL: ${{ secrets.MASTODON_URL }}
|
||||
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
|
||||
with:
|
||||
message: |
|
||||
This is a test toot, automatically posted by a github action.
|
||||
|
||||
${{ github.server_url }}/${{ github.repository }}
|
||||
|
||||
${{ github.ref }}
|
||||
@@ -109,3 +109,4 @@ lcov.info
|
||||
|
||||
.vim
|
||||
.vscode
|
||||
uv.lock
|
||||
|
||||
+10
-11
@@ -7,13 +7,17 @@ version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-lts-latest
|
||||
tools:
|
||||
python: "3.10"
|
||||
# You can also specify other tool versions:
|
||||
# nodejs: "16"
|
||||
# rust: "1.55"
|
||||
# golang: "1.17"
|
||||
python: "3.13"
|
||||
jobs:
|
||||
pre_install:
|
||||
- pip install --upgrade pip
|
||||
- pip install --upgrade --upgrade-strategy only-if-needed --group doc .
|
||||
post_build:
|
||||
- echo "Command run at 'post_build' step"
|
||||
- echo `date`
|
||||
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
@@ -22,8 +26,3 @@ sphinx:
|
||||
#If using Sphinx, optionally build your docs in additional formats such as PDF
|
||||
formats:
|
||||
- pdf
|
||||
|
||||
#Optionally declare the Python requirements required to build your docs
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
|
||||
+35
-1
@@ -1,4 +1,6 @@
|
||||
# Contributing to ptulsconv
|
||||
# Contributing
|
||||
|
||||
Contributions to `ptulsconv` are welcome!
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -7,3 +9,35 @@ Before submitting PRs or patches, please make sure your branch passes all of the
|
||||
```sh
|
||||
~/ptulsconv$ pytest
|
||||
```
|
||||
|
||||
## Regarding use of Agents
|
||||
|
||||
`ptulsconv` is an open-source project that is offered free for no commerical gain, and
|
||||
is developed and maintained for educational and creative reasons.
|
||||
|
||||
If you use an agent or LLM to produce code for it you are missing out on the benefits
|
||||
of contributing to an open-source project, particularly community, collaboration with
|
||||
other developers and designers, and being able to learn and experiment without the
|
||||
burden of deadlines or worrying about business cases or profits.
|
||||
|
||||
This project is supposed to be fun, do not let machines have fun for you.
|
||||
|
||||
We can't prevent you from using LLMs to contribute to this project but we ask you
|
||||
abide by the following eitiquette when doing so:
|
||||
|
||||
* All communication with the maintainers must be written by a human in their own
|
||||
voice. Never use an LLM to craft thread comments, discussion posts, issues, emails
|
||||
or other correspondence with other developers or the maintainers.
|
||||
* PRs must be submitted by a person. Do not allow an agent to submit its own PRs to
|
||||
this project.
|
||||
* Especially if you are a new contributor to this project, please submit only one PR
|
||||
at a time and please restrict the subject matter of the PR to a specific unit,
|
||||
module or tool. All submissions have to be reviewed and understood by the
|
||||
maintainers before they can be merged.
|
||||
|
||||
Obviously we can't verify if you follow all of these rules but certain telltale
|
||||
traits of LLM-predicted text or code will raise a flag: lack of brevity in
|
||||
descriptions or code comments, large amounts of text describing your process or
|
||||
steps that add little to understanding the changes you've made, use of an
|
||||
obsequious tone or being excessively accomodating, immediately doing requests
|
||||
without further discussion or clarifications.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||

|
||||

|
||||
[][pypi]
|
||||

|
||||

|
||||
[](https://github.com/iluvcapra/ptulsconv/actions/workflows/python-package.yml)
|
||||
|
||||
[pypi]: https://pypi.org/project/ptulsconv/
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
# ptulsconv
|
||||
|
||||
Read Pro Tools text exports and generate PDF reports, JSON output.
|
||||
Parse Pro Tools text exports and generate PDF reports, JSON output.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -23,6 +23,10 @@ The easiest way to install on your site is to use `pip`:
|
||||
|
||||
% pip3 install ptulsconv
|
||||
|
||||
If you are using `uv` you can also do a tool install...
|
||||
|
||||
% uv tool install ptulsconv
|
||||
|
||||
This will install the necessary libraries on your host and gives you
|
||||
command-line access to the tool through an entry-point `ptulsconv`. In a
|
||||
terminal window type `ptulsconv -h` for a list of available options.
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
alabaster==0.7.12
|
||||
Babel==2.11.0
|
||||
certifi==2022.12.7
|
||||
charset-normalizer==2.1.1
|
||||
docutils==0.17.1
|
||||
idna==3.4
|
||||
imagesize==1.4.1
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.1
|
||||
packaging==21.3
|
||||
parsimonious==0.10.0
|
||||
Pillow==9.3.0
|
||||
Pygments==2.13.0
|
||||
pyparsing==3.0.9
|
||||
pytz==2022.6
|
||||
regex==2022.10.31
|
||||
reportlab==3.6.12
|
||||
requests==2.28.1
|
||||
snowballstemmer==2.2.0
|
||||
Sphinx==5.3.0
|
||||
sphinx-rtd-theme==1.1.1
|
||||
sphinxcontrib-applehelp==1.0.2
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
sphinxcontrib-htmlhelp==2.0.0
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
sphinxcontrib-serializinghtml==1.1.5
|
||||
tqdm==4.64.1
|
||||
urllib3==1.26.12
|
||||
+17
-19
@@ -3,51 +3,49 @@
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
import sys
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
print(sys.path)
|
||||
|
||||
import ptulsconv
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = 'ptulsconv'
|
||||
copyright = ptulsconv.__copyright__
|
||||
author = ptulsconv.__author__
|
||||
release = ptulsconv.__version__
|
||||
project = "ptulsconv"
|
||||
copyright = "2019-2025 Jamie Hardt. All rights reserved"
|
||||
version = "Version 2"
|
||||
release = importlib.metadata.version("ptulsconv")
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.githubpages',
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.githubpages",
|
||||
]
|
||||
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
exclude_patterns = []
|
||||
|
||||
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_static_path = ['_static']
|
||||
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_static_path = ["_static"]
|
||||
|
||||
|
||||
latex_documents = [
|
||||
(master_doc, 'ptulsconv.tex', u'ptulsconv Documentation',
|
||||
u'Jamie Hardt', 'manual'),
|
||||
(master_doc, "ptulsconv.tex", "ptulsconv Documentation", "Jamie Hardt", "manual"),
|
||||
]
|
||||
|
||||
|
||||
@@ -66,7 +64,7 @@ epub_title = project
|
||||
# epub_uid = ''
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
epub_exclude_files = ["search.html"]
|
||||
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
|
||||
@@ -6,7 +6,12 @@ Usage 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
|
||||
|
||||
@@ -24,19 +24,21 @@ Step 2: Add More Information to Your Spots
|
||||
|
||||
Clips, tracks and markers in your session can contain additional 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,
|
||||
add the letters "$QN=" and then a unique number (any combination of letters
|
||||
or numbers that don't contain a space). You can type these yourself or add
|
||||
them with batch-renaming when you're done spotting.
|
||||
* **Every ADR clip must have a unique cue number.** After the name of each
|
||||
clip, add the letters ``$QN=`` and then a unique number (any combination of
|
||||
letters or numbers that don't contain a space). You can type these yourself
|
||||
or add them with batch-renaming when you're done spotting.
|
||||
* 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="
|
||||
to your clip names after the prompt and then some short text describing the
|
||||
reason, and then a closing "}". You can type anything, including spaces.
|
||||
* If a line is a TV cover line, you can add the text "[TV]" to the end.
|
||||
why you're replacing a particular line. Do this by adding the the text
|
||||
``{R=`` to your clip names after the prompt and then some short text
|
||||
describing the reason, and then a closing ``}``. You can type anything,
|
||||
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
|
||||
"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
|
||||
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
|
||||
they should be collated. Add "$CN=xxx" with a unique number to each track (or
|
||||
the track's comments.)
|
||||
they should be collated. Add ``$CN=xxx`` with
|
||||
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
|
||||
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.
|
||||
|
||||
|
||||
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
|
||||
using the Timecode time format.
|
||||
In Pro Tools, select the tracks that contain your spot clips.
|
||||
|
||||
Do not export crossfades.
|
||||
Then, in your Terminal, run the following command::
|
||||
|
||||
ptulsconv
|
||||
|
||||
Step 4: Run `ptulsconv` on the Text Export
|
||||
------------------------------------------
|
||||
|
||||
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
|
||||
`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
|
||||
folder it will create several PDFs and folders:
|
||||
|
||||
- "TITLE ADR Report" 📄 a PDF tabular report of every ADR line you've spotted.
|
||||
|
||||
@@ -4,8 +4,8 @@ Tagging
|
||||
=======
|
||||
|
||||
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
|
||||
any kind of extra data to a clip.
|
||||
tagging system in `ptulsconv` is flexible and can be used to add any kind of
|
||||
extra data to a clip.
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
@@ -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:
|
||||
* `$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`
|
||||
in this case may contain spaces or any chartacter up to the 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.
|
||||
|
||||
* ``$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`` in this case may contain spaces or any chartacter up to the
|
||||
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:::
|
||||
|
||||
`"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:
|
||||
|
||||
@@ -63,7 +67,7 @@ Fields in Track Names and Markers
|
||||
---------------------------------
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
-------------------------------------
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
@@ -108,19 +115,20 @@ Setting Document Options
|
||||
.. note::
|
||||
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 list of words
|
||||
..
|
||||
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 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.
|
||||
|
||||
font {NAME=`name`} {PATH=`path`}
|
||||
font {NAME=`name`} {PATH=`path`}
|
||||
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
|
||||
document it will be replaced with "replacement text".
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
"""
|
||||
Parse and convert Pro Tools text exports
|
||||
"""
|
||||
|
||||
__version__ = '1.0.6'
|
||||
# __author__ = 'Jamie Hardt'
|
||||
# __license__ = 'MIT'
|
||||
# __copyright__ = "%s %s (c) 2022 %s. All rights reserved." % (__name__, __version__, __author__)
|
||||
@@ -1,110 +0,0 @@
|
||||
from optparse import OptionParser, OptionGroup
|
||||
import datetime
|
||||
import sys
|
||||
|
||||
from ptulsconv import __name__, __version__, __author__,__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)
|
||||
|
||||
if len(args) < 2:
|
||||
print_fatal_error("Error: No input file")
|
||||
parser.print_help(sys.stderr)
|
||||
sys.exit(22)
|
||||
|
||||
try:
|
||||
major_mode = options.output_format
|
||||
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()
|
||||
@@ -1,222 +0,0 @@
|
||||
"""
|
||||
This module provides the main input document parsing and transform
|
||||
implementation.
|
||||
"""
|
||||
import datetime
|
||||
import os
|
||||
|
||||
import sys
|
||||
from itertools import chain
|
||||
import csv
|
||||
from typing import List
|
||||
from fractions import Fraction
|
||||
|
||||
from .docparser.adr_entity import make_entities
|
||||
from .reporting import print_section_header_style, print_status_style, print_warning
|
||||
from .validations import *
|
||||
|
||||
from ptulsconv.docparser import parse_document
|
||||
from ptulsconv.docparser.tag_compiler import TagCompiler
|
||||
from ptulsconv.broadcast_timecode import TimecodeFormat
|
||||
from fractions import Fraction
|
||||
|
||||
from ptulsconv.pdf.supervisor_1pg import output_report as output_supervisor_1pg
|
||||
from ptulsconv.pdf.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 json import JSONEncoder
|
||||
|
||||
|
||||
class MyEncoder(JSONEncoder):
|
||||
"""
|
||||
A subclass of :class:`JSONEncoder` which encodes :class:`Fraction` objects
|
||||
as a dict.
|
||||
"""
|
||||
force_denominator: Optional[int]
|
||||
|
||||
def default(self, o):
|
||||
"""
|
||||
|
||||
"""
|
||||
if isinstance(o, Fraction):
|
||||
return dict(numerator=o.numerator, denominator=o.denominator)
|
||||
else:
|
||||
return o.__dict__
|
||||
|
||||
|
||||
def output_adr_csv(lines: List[ADRLine], time_format: TimecodeFormat):
|
||||
"""
|
||||
Writes ADR lines as CSV to the current working directory. Creates directories
|
||||
for each character number and name pair, and within that directory, creates
|
||||
a CSV file for each reel.
|
||||
"""
|
||||
reels = set([ln.reel for ln in lines])
|
||||
|
||||
for n, name in [(n.character_id, n.character_name) for n in lines]:
|
||||
dir_name = "%s_%s" % (n, name)
|
||||
os.makedirs(dir_name, exist_ok=True)
|
||||
os.chdir(dir_name)
|
||||
for reel in reels:
|
||||
these_lines = [ln for ln in lines if ln.character_id == n and ln.reel == reel]
|
||||
|
||||
if len(these_lines) == 0:
|
||||
continue
|
||||
|
||||
outfile_name = "%s_%s_%s_%s.csv" % (these_lines[0].title, n, these_lines[0].character_name, reel,)
|
||||
|
||||
with open(outfile_name, mode='w', newline='') as outfile:
|
||||
writer = csv.writer(outfile, dialect='excel')
|
||||
writer.writerow(['Title', 'Character Name', 'Cue Number',
|
||||
'Reel', 'Version',
|
||||
'Start', 'Finish',
|
||||
'Start Seconds', 'Finish Seconds',
|
||||
'Prompt',
|
||||
'Reason', 'Note', 'TV'])
|
||||
|
||||
for event in these_lines:
|
||||
this_start = event.start or 0
|
||||
this_finish = event.finish or 0
|
||||
this_row = [event.title, event.character_name, event.cue_number,
|
||||
event.reel, event.version,
|
||||
time_format.seconds_to_smpte(this_start), 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)
|
||||
os.chdir("..")
|
||||
|
||||
|
||||
def generate_documents(session_tc_format, scenes, adr_lines: Iterator[ADRLine], title):
|
||||
"""
|
||||
Create PDF output.
|
||||
"""
|
||||
print_section_header_style("Creating PDF Reports")
|
||||
report_date = datetime.datetime.now()
|
||||
reports_dir = "%s_%s" % (title, report_date.strftime("%Y-%m-%d_%H%M%S"))
|
||||
os.makedirs(reports_dir, exist_ok=False)
|
||||
os.chdir(reports_dir)
|
||||
|
||||
client = next((x.client for x in adr_lines), "")
|
||||
supervisor = next((x.supervisor for x in adr_lines), "")
|
||||
|
||||
output_continuity(scenes=scenes, tc_display_format=session_tc_format,
|
||||
title=title, client=client, supervisor=supervisor)
|
||||
|
||||
# reels = sorted([r for r in compiler.compile_all_time_spans() if r[0] == 'Reel'],
|
||||
# key=lambda x: x[2])
|
||||
reels = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6']
|
||||
|
||||
if len(adr_lines) == 0:
|
||||
print_status_style("No ADR lines were found in the "
|
||||
"input document. ADR reports will not be generated.")
|
||||
|
||||
else:
|
||||
create_adr_reports(adr_lines, tc_display_format=session_tc_format,
|
||||
reel_list=sorted(reels))
|
||||
|
||||
|
||||
def create_adr_reports(lines: List[ADRLine], tc_display_format: TimecodeFormat, reel_list: List[str]):
|
||||
"""
|
||||
Creates a directory heirarchy and a respective set of ADR reports,
|
||||
given a list of lines.
|
||||
"""
|
||||
|
||||
print_status_style("Creating ADR Report")
|
||||
output_summary(lines, tc_display_format=tc_display_format)
|
||||
|
||||
print_status_style("Creating Line Count")
|
||||
output_line_count(lines, reel_list=reel_list)
|
||||
|
||||
print_status_style("Creating Supervisor Logs directory and reports")
|
||||
os.makedirs("Supervisor Logs", exist_ok=True)
|
||||
os.chdir("Supervisor Logs")
|
||||
output_supervisor_1pg(lines, tc_display_format=tc_display_format)
|
||||
os.chdir("..")
|
||||
|
||||
print_status_style("Creating Director's Logs director and reports")
|
||||
os.makedirs("Director Logs", exist_ok=True)
|
||||
os.chdir("Director Logs")
|
||||
output_summary(lines, tc_display_format=tc_display_format, by_character=True)
|
||||
os.chdir("..")
|
||||
|
||||
print_status_style("Creating CSV outputs")
|
||||
os.makedirs("CSV", exist_ok=True)
|
||||
os.chdir("CSV")
|
||||
output_adr_csv(lines, time_format=tc_display_format)
|
||||
os.chdir("..")
|
||||
|
||||
print_status_style("Creating Scripts directory and reports")
|
||||
os.makedirs("Talent Scripts", exist_ok=True)
|
||||
os.chdir("Talent Scripts")
|
||||
output_talent_sides(lines, tc_display_format=tc_display_format)
|
||||
|
||||
|
||||
def convert(input_file, major_mode, output=sys.stdout, warnings=True):
|
||||
"""
|
||||
Primary worker function, accepts the input file and decides
|
||||
what to do with it based on the `major_mode`.
|
||||
|
||||
:param input_file: a path to the input file.
|
||||
:param major_mode: the selected output mode, 'raw', 'tagged' or 'doc'.
|
||||
"""
|
||||
session = parse_document(input_file)
|
||||
session_tc_format = session.header.timecode_format
|
||||
|
||||
if major_mode == 'raw':
|
||||
output.write(MyEncoder().encode(session))
|
||||
|
||||
else:
|
||||
compiler = TagCompiler()
|
||||
compiler.session = session
|
||||
compiled_events = list(compiler.compile_events())
|
||||
|
||||
if major_mode == 'tagged':
|
||||
output.write(MyEncoder().encode(compiled_events))
|
||||
|
||||
elif major_mode == 'doc':
|
||||
generic_events, adr_lines = make_entities(compiled_events)
|
||||
|
||||
scenes = sorted([s for s in compiler.compile_all_time_spans() if s[0] == 'Sc'],
|
||||
key=lambda x: x[2])
|
||||
|
||||
# TODO: Breakdown by titles
|
||||
titles = set([x.title for x in (generic_events + adr_lines)])
|
||||
if len(titles) != 1:
|
||||
print_warning("Multiple titles per export is not supported, "
|
||||
"found multiple titles: %s Exiting." % titles)
|
||||
exit(-1)
|
||||
|
||||
title = list(titles)[0]
|
||||
|
||||
print_status_style("%i generic events found." % len(generic_events))
|
||||
print_status_style("%i ADR events found." % len(adr_lines))
|
||||
|
||||
if warnings:
|
||||
perform_adr_validations(adr_lines)
|
||||
|
||||
generate_documents(session_tc_format, scenes, adr_lines, title)
|
||||
|
||||
|
||||
def perform_adr_validations(lines : Iterator[ADRLine]):
|
||||
"""
|
||||
Performs validations on the input.
|
||||
"""
|
||||
for warning in chain(validate_unique_field(lines,
|
||||
field='cue_number',
|
||||
scope='title'),
|
||||
validate_non_empty_field(lines,
|
||||
field='cue_number'),
|
||||
validate_non_empty_field(lines,
|
||||
field='character_id'),
|
||||
validate_non_empty_field(lines,
|
||||
field='title'),
|
||||
validate_dependent_value(lines,
|
||||
key_field='character_id',
|
||||
dependent_field='character_name'),
|
||||
validate_dependent_value(lines,
|
||||
key_field='character_id',
|
||||
dependent_field='actor_name')):
|
||||
print_warning(warning.report_message())
|
||||
@@ -1,5 +0,0 @@
|
||||
"""
|
||||
Docparser module
|
||||
"""
|
||||
|
||||
from .pt_doc_parser import parse_document
|
||||
@@ -1,135 +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))
|
||||
]
|
||||
|
||||
|
||||
@@ -1,175 +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']
|
||||
@@ -1 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
@@ -1,246 +0,0 @@
|
||||
from parsimonious.nodes import NodeVisitor
|
||||
from parsimonious.grammar import Grammar
|
||||
|
||||
from .doc_entity import SessionDescriptor, HeaderDescriptor, TrackDescriptor, FileDescriptor, \
|
||||
TrackClipDescriptor, ClipDescriptor, PluginDescriptor, MarkerDescriptor
|
||||
|
||||
|
||||
protools_text_export_grammar = Grammar(
|
||||
r"""
|
||||
document = header files_section? clips_section? plugin_listing? track_listing? markers_listing?
|
||||
header = "SESSION NAME:" fs string_value rs
|
||||
"SAMPLE RATE:" fs float_value rs
|
||||
"BIT DEPTH:" fs integer_value "-bit" rs
|
||||
"SESSION START TIMECODE:" fs string_value rs
|
||||
"TIMECODE FORMAT:" fs frame_rate " Drop"? " Frame" rs
|
||||
"# OF AUDIO TRACKS:" fs integer_value rs
|
||||
"# OF AUDIO CLIPS:" fs integer_value rs
|
||||
"# OF AUDIO FILES:" fs integer_value rs block_ending
|
||||
|
||||
frame_rate = ("60" / "59.94" / "30" / "29.97" / "25" / "24" / "23.976")
|
||||
files_section = files_header files_column_header file_record* block_ending
|
||||
files_header = "F I L E S I N S E S S I O N" rs
|
||||
files_column_header = "Filename" isp fs "Location" rs
|
||||
file_record = string_value fs string_value rs
|
||||
|
||||
clips_section = clips_header clips_column_header clip_record* block_ending
|
||||
clips_header = "O N L I N E C L I P S I N S E S S I O N" rs
|
||||
clips_column_header = string_value fs string_value rs
|
||||
clip_record = string_value fs string_value (fs "[" integer_value "]")? rs
|
||||
|
||||
plugin_listing = plugin_header plugin_column_header plugin_record* block_ending
|
||||
plugin_header = "P L U G - I N S L I S T I N G" rs
|
||||
plugin_column_header = "MANUFACTURER " fs "PLUG-IN NAME " fs
|
||||
"VERSION " fs "FORMAT " fs "STEMS " fs
|
||||
"NUMBER OF INSTANCES" rs
|
||||
plugin_record = string_value fs string_value fs string_value fs
|
||||
string_value fs string_value fs string_value rs
|
||||
|
||||
track_listing = track_listing_header track_block*
|
||||
track_block = track_list_top ( track_clip_entry / block_ending )*
|
||||
|
||||
track_listing_header = "T R A C K L I S T I N G" rs
|
||||
track_list_top = "TRACK NAME:" fs string_value rs
|
||||
"COMMENTS:" fs string_value rs
|
||||
"USER DELAY:" fs integer_value " Samples" rs
|
||||
"STATE: " track_state_list rs
|
||||
("PLUG-INS: " ( fs string_value )* rs)?
|
||||
"CHANNEL " fs "EVENT " fs "CLIP NAME " fs
|
||||
"START TIME " fs "END TIME " fs "DURATION " fs
|
||||
("TIMESTAMP " fs)? "STATE" rs
|
||||
|
||||
track_state_list = (track_state " ")*
|
||||
|
||||
track_state = "Solo" / "Muted" / "Inactive" / "Hidden"
|
||||
|
||||
track_clip_entry = integer_value isp fs
|
||||
integer_value isp fs
|
||||
string_value fs
|
||||
string_value fs string_value fs string_value fs (string_value fs)?
|
||||
track_clip_state rs
|
||||
|
||||
track_clip_state = ("Muted" / "Unmuted")
|
||||
|
||||
markers_listing = markers_listing_header markers_column_header marker_record*
|
||||
markers_listing_header = "M A R K E R S L I S T I N G" rs
|
||||
markers_column_header = "# " fs "LOCATION " fs "TIME REFERENCE " fs
|
||||
"UNITS " fs "NAME " fs "COMMENTS" rs
|
||||
|
||||
marker_record = integer_value isp fs string_value fs integer_value isp fs
|
||||
string_value fs string_value fs string_value rs
|
||||
|
||||
fs = "\t"
|
||||
rs = "\n"
|
||||
block_ending = rs rs
|
||||
string_value = ~r"[^\t\n]*"
|
||||
integer_value = ~r"\d+"
|
||||
float_value = ~r"\d+(\.\d+)?"
|
||||
isp = ~r"[^\d\t\n]*"
|
||||
""")
|
||||
|
||||
|
||||
def parse_document(path: str) -> SessionDescriptor:
|
||||
"""
|
||||
Parse a Pro Tools text export.
|
||||
:param path: path to a file
|
||||
:return: the session descriptor
|
||||
"""
|
||||
with open(path, 'r') as f:
|
||||
ast = protools_text_export_grammar.parse(f.read())
|
||||
return DocParserVisitor().visit(ast)
|
||||
|
||||
|
||||
class DocParserVisitor(NodeVisitor):
|
||||
|
||||
@staticmethod
|
||||
def visit_document(_, visited_children) -> SessionDescriptor:
|
||||
files = next(iter(visited_children[1]), None)
|
||||
clips = next(iter(visited_children[2]), None)
|
||||
plugins = next(iter(visited_children[3]), None)
|
||||
tracks = next(iter(visited_children[4]), None)
|
||||
markers = next(iter(visited_children[5]), None)
|
||||
|
||||
return SessionDescriptor(header=visited_children[0],
|
||||
files=files,
|
||||
clips=clips,
|
||||
plugins=plugins,
|
||||
tracks=tracks,
|
||||
markers=markers)
|
||||
|
||||
@staticmethod
|
||||
def visit_header(_, visited_children):
|
||||
|
||||
tc_drop = False
|
||||
for _ in visited_children[20]:
|
||||
tc_drop = True
|
||||
|
||||
return HeaderDescriptor(session_name=visited_children[2],
|
||||
sample_rate=visited_children[6],
|
||||
bit_depth=visited_children[10],
|
||||
start_timecode=visited_children[15],
|
||||
timecode_format=visited_children[19],
|
||||
timecode_drop_frame=tc_drop,
|
||||
count_audio_tracks=visited_children[25],
|
||||
count_clips=visited_children[29],
|
||||
count_files=visited_children[33])
|
||||
|
||||
@staticmethod
|
||||
def visit_files_section(_, visited_children):
|
||||
return list(map(lambda child: FileDescriptor(filename=child[0], path=child[2]), visited_children[2]))
|
||||
|
||||
@staticmethod
|
||||
def visit_clips_section(_, visited_children):
|
||||
channel = next(iter(visited_children[2][3]), 1)
|
||||
|
||||
return list(map(lambda child: ClipDescriptor(clip_name=child[0], file=child[2], channel=channel),
|
||||
visited_children[2]))
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
def visit_track_block(_, visited_children):
|
||||
track_header, track_clip_list = visited_children
|
||||
clips = []
|
||||
for clip in track_clip_list:
|
||||
if clip[0] is not None:
|
||||
clips.append(clip[0])
|
||||
|
||||
plugins = []
|
||||
for plugin_opt in track_header[16]:
|
||||
for plugin in plugin_opt[1]:
|
||||
plugins.append(plugin[1])
|
||||
|
||||
return TrackDescriptor(
|
||||
name=track_header[2],
|
||||
comments=track_header[6],
|
||||
user_delay_samples=track_header[10],
|
||||
state=track_header[14],
|
||||
plugins=plugins,
|
||||
clips=clips
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def visit_frame_rate(node, _):
|
||||
return node.text
|
||||
|
||||
@staticmethod
|
||||
def visit_track_listing(_, visited_children):
|
||||
return visited_children[1]
|
||||
|
||||
@staticmethod
|
||||
def visit_track_clip_entry(_, visited_children):
|
||||
timestamp = None
|
||||
if isinstance(visited_children[14], list):
|
||||
timestamp = visited_children[14][0][0]
|
||||
|
||||
return TrackClipDescriptor(channel=visited_children[0],
|
||||
event=visited_children[3],
|
||||
clip_name=visited_children[6],
|
||||
start_time=visited_children[8],
|
||||
finish_time=visited_children[10],
|
||||
duration=visited_children[12],
|
||||
timestamp=timestamp,
|
||||
state=visited_children[15])
|
||||
|
||||
@staticmethod
|
||||
def visit_track_state_list(_, visited_children):
|
||||
states = []
|
||||
for next_state in visited_children:
|
||||
states.append(next_state[0][0].text)
|
||||
return states
|
||||
|
||||
@staticmethod
|
||||
def visit_track_clip_state(node, _):
|
||||
return node.text
|
||||
|
||||
@staticmethod
|
||||
def visit_markers_listing(_, visited_children):
|
||||
markers = []
|
||||
|
||||
for marker in visited_children[2]:
|
||||
markers.append(marker)
|
||||
|
||||
return markers
|
||||
|
||||
@staticmethod
|
||||
def visit_marker_record(_, 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])
|
||||
|
||||
@staticmethod
|
||||
def visit_formatted_clip_name(_, visited_children):
|
||||
return visited_children[1].text
|
||||
|
||||
@staticmethod
|
||||
def visit_string_value(node, _):
|
||||
return node.text.strip(" ")
|
||||
|
||||
@staticmethod
|
||||
def visit_integer_value(node, _):
|
||||
return int(node.text)
|
||||
|
||||
# def visit_timecode_value(self, node, visited_children):
|
||||
# return node.text.strip(" ")
|
||||
|
||||
@staticmethod
|
||||
def visit_float_value(node, _):
|
||||
return float(node.text)
|
||||
|
||||
def visit_block_ending(self, node, visited_children):
|
||||
pass
|
||||
|
||||
def generic_visit(self, node, visited_children):
|
||||
""" The generic visit method. """
|
||||
return visited_children or node
|
||||
@@ -1,57 +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, Spacer
|
||||
|
||||
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)
|
||||
@@ -1,246 +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:
|
||||
# columns.append({
|
||||
# 'heading': 'RX',
|
||||
# 'value_getter': lambda recs: blank_len([r for r in recs if 'Reel' not in r.keys()]),
|
||||
# 'value_getter2': lambda recs: time_format(sum([r.get('Time Budget Mins', 0.) for r in recs
|
||||
# if 'Reel' not in r.keys()])),
|
||||
# 'style_getter': lambda col_index: [('ALIGN', (col_index, 0), (col_index, -1), 'CENTER')],
|
||||
# 'width': num_column_width
|
||||
# })
|
||||
|
||||
for n in reel_numbers:
|
||||
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]), zero_str=None),
|
||||
'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)
|
||||
@@ -1,149 +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)
|
||||
@@ -1,258 +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, footage_to_frame_count
|
||||
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)
|
||||
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)
|
||||
@@ -1,76 +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,
|
||||
supervisor=char_lines[0].supervisor,
|
||||
client=char_lines[0].client,
|
||||
document_header=character_name)
|
||||
|
||||
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)
|
||||
@@ -1,69 +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)
|
||||
@@ -1,74 +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)
|
||||
@@ -1,164 +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)
|
||||
+48
-24
@@ -1,43 +1,67 @@
|
||||
[build-system]
|
||||
requires = ["flit_core >=3.2,<4"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
|
||||
[project]
|
||||
name = "ptulsconv"
|
||||
authors = [
|
||||
{name = "Jamie Hardt", email = "jamiehardt@me.com"},
|
||||
]
|
||||
version = "4.0.2"
|
||||
description = "Read Pro Tools Text exports and generate PDF ADR Reports, JSON"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
requires-python = ">=3.9"
|
||||
|
||||
license-files = ["LICENSE"]
|
||||
keywords = ["text-processing", "parsers", "film",
|
||||
"broadcast", "editing", "editorial"]
|
||||
classifiers = [
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Topic :: Multimedia',
|
||||
'Topic :: Multimedia :: Sound/Audio',
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Topic :: Text Processing :: Filters"
|
||||
]
|
||||
requires-python = ">=3.7"
|
||||
dynamic = ["version", "description"]
|
||||
keywords = ["text-processing", "parsers", "film",
|
||||
"broadcast", "editing", "editorial"]
|
||||
dependencies = ['parsimonious', 'tqdm', 'reportlab']
|
||||
|
||||
[project.optional-dependencies]
|
||||
doc = [
|
||||
"Sphinx ~= 5.3.0",
|
||||
"sphinx-rtd-theme >= 1.1.1"
|
||||
authors = [{name = "Jamie Hardt", email = "<jamiehardt@me.com>"}]
|
||||
dependencies = [
|
||||
"parsimonious>=0.10.0",
|
||||
"py-ptsl>=600.0.0",
|
||||
"reportlab>=4.4.4",
|
||||
"tqdm>=4.67.1",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
flit = "ptulsconv.__main__:main"
|
||||
|
||||
# [tool.uv.build-backend]
|
||||
# data = { headers = "include/headers", scripts = "bin" }
|
||||
|
||||
[project.urls]
|
||||
Source = 'https://github.com/iluvcapra/ptulsconv'
|
||||
Issues = 'https://github.com/iluvcapra/ptulsconv/issues'
|
||||
Documentation = 'https://ptulsconv.readthedocs.io/'
|
||||
|
||||
[project.optional-dependencies]
|
||||
doc = [
|
||||
"sphinx>=7.4.7",
|
||||
"sphinx-rtd-theme>=3.0.2",
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
ignore = [
|
||||
"DTZ005" # datetime created without a tz
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"reaper/Export Items as Text.py" = ["F821"]
|
||||
|
||||
[project.scripts]
|
||||
ptulsconv = "ptulsconv:__main__.main"
|
||||
|
||||
[build-system]
|
||||
requires = ["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",
|
||||
]
|
||||
|
||||
@@ -1,47 +1,68 @@
|
||||
# type: ignore[reportUndefinedVariable]
|
||||
|
||||
# Export Items as Text.py
|
||||
# (c) 2021 Jamie Hardt. All rights reserved.
|
||||
#
|
||||
#
|
||||
|
||||
import json
|
||||
import os.path
|
||||
import datetime
|
||||
import json
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
item_records = list()
|
||||
item_records = []
|
||||
|
||||
for i in range(0, RPR_CountMediaItems(0) ):
|
||||
for i in range(RPR_CountMediaItems(0)):
|
||||
this_item = RPR_GetMediaItem(0, i)
|
||||
|
||||
item_record = {}
|
||||
item_record["mute"] = True if RPR_GetMediaItemInfo_Value(this_item, "B_MUTE_ACTUAL") > 0. else False
|
||||
item_record["mute"] = RPR_GetMediaItemInfo_Value(this_item, "B_MUTE_ACTUAL") > 0.0
|
||||
|
||||
item_record["duration"] = RPR_GetMediaItemInfo_Value(this_item, "D_LENGTH")
|
||||
_, item_record["duration_tc"], _, _, _ = RPR_format_timestr_len(item_record["duration"], "", 128, 0., 5)
|
||||
_, item_record["duration_tc"], _, _, _ = RPR_format_timestr_len(
|
||||
item_record["duration"], "", 128, 0.0, 5
|
||||
)
|
||||
|
||||
item_record["position"] = RPR_GetMediaItemInfo_Value(this_item, "D_POSITION")
|
||||
_, item_record["position_tc"], _, _ = RPR_format_timestr_pos(item_record["position"], "", 128, 5)
|
||||
_, item_record["position_tc"], _, _ = RPR_format_timestr_pos(
|
||||
item_record["position"], "", 128, 5
|
||||
)
|
||||
|
||||
item_record["selected"] = True if RPR_GetMediaItemInfo_Value(this_item, "B_UISEL") > 0. else False
|
||||
_, _, _, item_record["notes"], _ = RPR_GetSetMediaItemInfo_String(this_item, "P_NOTES", "", False)
|
||||
_, _, _, item_record["item_guid"], _ = RPR_GetSetMediaItemInfo_String(this_item, "GUID", "", False)
|
||||
item_record["selected"] = RPR_GetMediaItemInfo_Value(this_item, "B_UISEL") > 0.0
|
||||
_, _, _, item_record["notes"], _ = RPR_GetSetMediaItemInfo_String(
|
||||
this_item, "P_NOTES", "", False
|
||||
)
|
||||
_, _, _, item_record["item_guid"], _ = RPR_GetSetMediaItemInfo_String(
|
||||
this_item, "GUID", "", False
|
||||
)
|
||||
|
||||
active_take = RPR_GetActiveTake(this_item)
|
||||
_, _, _, item_record["active_take_name"], _ = RPR_GetSetMediaItemTakeInfo_String(active_take, "P_NAME", "", False)
|
||||
_, _, _, item_record["active_take_guid"], _ = RPR_GetSetMediaItemTakeInfo_String(active_take, "GUID", "", False)
|
||||
_, _, _, item_record["active_take_name"], _ = RPR_GetSetMediaItemTakeInfo_String(
|
||||
active_take, "P_NAME", "", False
|
||||
)
|
||||
_, _, _, item_record["active_take_guid"], _ = RPR_GetSetMediaItemTakeInfo_String(
|
||||
active_take, "GUID", "", False
|
||||
)
|
||||
|
||||
item_track = RPR_GetMediaItemTrack(this_item)
|
||||
_, _, _, item_record["track_name"], _ = RPR_GetSetMediaTrackInfo_String(item_track, "P_NAME", "", False)
|
||||
_, _, _, item_record["track_guid"], _ = RPR_GetSetMediaTrackInfo_String(item_track, "GUID", "", False)
|
||||
item_record["track_index"] = RPR_GetMediaTrackInfo_Value(item_track, "IP_TRACKNUMBER")
|
||||
item_record["track_muted"] = True if RPR_GetMediaTrackInfo_Value(item_track, "B_MUTE") > 0. else False
|
||||
_, _, _, item_record["track_name"], _ = RPR_GetSetMediaTrackInfo_String(
|
||||
item_track, "P_NAME", "", False
|
||||
)
|
||||
_, _, _, item_record["track_guid"], _ = RPR_GetSetMediaTrackInfo_String(
|
||||
item_track, "GUID", "", False
|
||||
)
|
||||
item_record["track_index"] = RPR_GetMediaTrackInfo_Value(
|
||||
item_track, "IP_TRACKNUMBER"
|
||||
)
|
||||
item_record["track_muted"] = RPR_GetMediaTrackInfo_Value(item_track, "B_MUTE") > 0.0
|
||||
|
||||
item_records = item_records + [item_record]
|
||||
|
||||
output = dict()
|
||||
output = {}
|
||||
output["items"] = item_records
|
||||
_, output["project_title"], _ = RPR_GetProjectName(0, "", 1024)
|
||||
_, _, output["project_author"], _ = RPR_GetSetProjectAuthor(0, False, "", 1024)
|
||||
output["project_frame_rate"], _, output["project_drop_frame"] = RPR_TimeMap_curFrameRate(0, True)
|
||||
output["project_frame_rate"], _, output["project_drop_frame"] = (
|
||||
RPR_TimeMap_curFrameRate(0, True)
|
||||
)
|
||||
|
||||
output_path, _ = RPR_GetProjectPath("", 1024)
|
||||
|
||||
@@ -51,12 +72,15 @@ output_title = output["project_title"]
|
||||
if output_title == "":
|
||||
output_title = "unsaved project"
|
||||
|
||||
output_file_name = "%s Text Export %s.txt" % (output_title, now.strftime('%Y%m%d_%H%M'))
|
||||
output_file_name = f"{output_title} Text Export {now.strftime('%Y%m%d_%H%M')}.txt"
|
||||
output_path = output_path + "/" + output_file_name
|
||||
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(output, f, allow_nan=True, indent=4)
|
||||
|
||||
RPR_ShowMessageBox("Exported text file \"%s\" to project folder." % output_file_name, "Text Export Complete", 0)
|
||||
RPR_ShowMessageBox(
|
||||
f'Exported text file "{output_file_name}" to project folder. Text Export Complete',
|
||||
0,
|
||||
)
|
||||
|
||||
#RPR_ShowConsoleMsg(output_path)
|
||||
# RPR_ShowConsoleMsg(output_path)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Parse and convert Pro Tools text exports
|
||||
"""
|
||||
|
||||
__copyright__ = "ptulsconv (c) 2025 Jamie Hardt. All rights reserved."
|
||||
@@ -0,0 +1,126 @@
|
||||
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_section_header_style,
|
||||
print_status_style,
|
||||
)
|
||||
|
||||
|
||||
def dump_field_map(output=sys.stdout):
|
||||
from ptulsconv.docparser.tag_mapping import TagMapping
|
||||
|
||||
TagMapping.print_rules(GENERIC_TAG_MAPPING, output=output)
|
||||
TagMapping.print_rules(ADR_TAG_MAPPING, output=output)
|
||||
|
||||
|
||||
def dump_formats():
|
||||
print_section_header_style("`raw` format:")
|
||||
sys.stderr.write("A JSON document of the parsed Pro Tools export.\n")
|
||||
print_section_header_style("`tagged` Format:")
|
||||
sys.stderr.write(
|
||||
"A JSON document containing one record for each clip, with\n"
|
||||
"all tags parsed and all tagging rules applied. \n"
|
||||
)
|
||||
print_section_header_style("`doc` format:")
|
||||
sys.stderr.write(
|
||||
"Creates a directory with folders for different types\nof ADR reports.\n\n"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for the command-line invocation"""
|
||||
parser = OptionParser()
|
||||
parser.usage = "ptulsconv [options] [TEXT_EXPORT.txt]"
|
||||
|
||||
parser.add_option(
|
||||
"-f",
|
||||
"--format",
|
||||
dest="output_format",
|
||||
metavar="FMT",
|
||||
choices=["raw", "tagged", "doc"],
|
||||
default="doc",
|
||||
help="Set output format, `raw`, `tagged`, `doc`.",
|
||||
)
|
||||
|
||||
parser.add_option(
|
||||
"-m",
|
||||
"--movie-opts",
|
||||
dest="movie_opts",
|
||||
metavar="MOVIE_OPTS",
|
||||
help="Set movie options",
|
||||
)
|
||||
|
||||
warn_options = OptionGroup(title="Warning and Validation Options", parser=parser)
|
||||
|
||||
warn_options.add_option(
|
||||
"-W",
|
||||
action="store_false",
|
||||
dest="warnings",
|
||||
default=True,
|
||||
help="Suppress warnings for common errors (missing code numbers etc.)",
|
||||
)
|
||||
|
||||
parser.add_option_group(warn_options)
|
||||
|
||||
informational_options = OptionGroup(
|
||||
title="Informational Options",
|
||||
parser=parser,
|
||||
description="Print useful information and exit without processing input files.",
|
||||
)
|
||||
|
||||
informational_options.add_option(
|
||||
"--show-formats",
|
||||
dest="show_formats",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Display helpful information about the available output formats.",
|
||||
)
|
||||
|
||||
informational_options.add_option(
|
||||
"--show-available-tags",
|
||||
dest="show_tags",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Display tag mappings for the FMP XML output style and exit.",
|
||||
)
|
||||
|
||||
parser.add_option_group(informational_options)
|
||||
|
||||
version = importlib.metadata.version(ptulsconv.__name__)
|
||||
print_banner_style(f"{ptulsconv.__name__} - version {version}")
|
||||
print_banner_style(ptulsconv.__copyright__)
|
||||
|
||||
(options, args) = parser.parse_args(sys.argv)
|
||||
|
||||
print_section_header_style("Startup")
|
||||
print_status_style(f"This run started {datetime.datetime.now().isoformat()}")
|
||||
|
||||
if options.show_tags:
|
||||
dump_field_map()
|
||||
sys.exit(0)
|
||||
|
||||
elif options.show_formats:
|
||||
dump_formats()
|
||||
sys.exit(0)
|
||||
|
||||
major_mode = options.output_format
|
||||
|
||||
if len(args) < 2:
|
||||
print_status_style(
|
||||
"No input file provided, will connect to Pro Tools with PTSL..."
|
||||
)
|
||||
convert(major_mode=major_mode, warnings=options.warnings)
|
||||
else:
|
||||
convert(input_file=args[1], major_mode=major_mode, warnings=options.warnings)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,44 +2,54 @@
|
||||
Useful functions for parsing and working with timecode.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from fractions import Fraction
|
||||
from typing import Optional, SupportsFloat
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class TimecodeFormat(namedtuple("_TimecodeFormat", "frame_duration logical_fps drop_frame")):
|
||||
class TimecodeFormat(NamedTuple):
|
||||
"""
|
||||
A struct reperesenting a timecode datum.
|
||||
"""
|
||||
|
||||
def smpte_to_seconds(self, smpte: str) -> Optional[Fraction]:
|
||||
frame_count = smpte_to_frame_count(smpte, self.logical_fps, drop_frame_hint=self.drop_frame)
|
||||
frame_duration: Fraction
|
||||
logical_fps: int
|
||||
drop_frame: bool
|
||||
|
||||
def smpte_to_seconds(self, smpte: str) -> Fraction | None:
|
||||
frame_count = smpte_to_frame_count(
|
||||
smpte, self.logical_fps, drop_frame_hint=self.drop_frame
|
||||
)
|
||||
if frame_count is None:
|
||||
return None
|
||||
else:
|
||||
return frame_count * self.frame_duration
|
||||
|
||||
def seconds_to_smpte(self, seconds: SupportsFloat) -> str:
|
||||
def seconds_to_smpte(self, seconds: Fraction) -> str:
|
||||
frame_count = int(seconds / self.frame_duration)
|
||||
return frame_count_to_smpte(frame_count, self.logical_fps, self.drop_frame)
|
||||
|
||||
|
||||
def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int, drop_frame_hint=False) -> Optional[int]:
|
||||
def smpte_to_frame_count(
|
||||
smpte_rep_string: str, frames_per_logical_second: int, drop_frame_hint=False
|
||||
) -> int | None:
|
||||
"""
|
||||
Convert a string with a SMPTE timecode representation into a frame count.
|
||||
|
||||
:param smpte_rep_string: The timecode string
|
||||
:param frames_per_logical_second: Num of frames in a logical second. This is asserted to be
|
||||
in one of `[24,25,30,48,50,60]`
|
||||
:param drop_frame_hint: `True` if the timecode rep is drop frame. This is ignored (and implied `True`) if
|
||||
the last separator in the timecode string is a semicolon. This is ignored (and implied `False`) if
|
||||
:param frames_per_logical_second: Num of frames in a logical second. This
|
||||
is asserted to be in one of `[24,25,30,48,50,60]`
|
||||
:param drop_frame_hint: `True` if the timecode rep is drop frame. This is
|
||||
ignored (and implied `True`) if the last separator in the timecode
|
||||
string is a semicolon. This is ignored (and implied `False`) if
|
||||
`frames_per_logical_second` is not 30 or 60.
|
||||
"""
|
||||
assert frames_per_logical_second in [24, 25, 30, 48, 50, 60]
|
||||
|
||||
m = re.search(r'(\d?\d)[:;](\d\d)[:;](\d\d)([:;])(\d\d)(\.\d+)?', smpte_rep_string)
|
||||
m = re.search(r"(\d?\d)[:;](\d\d)[:;](\d\d)([:;])(\d\d)(\.\d+)?", smpte_rep_string)
|
||||
|
||||
if m is None:
|
||||
return None
|
||||
@@ -54,12 +64,16 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int,
|
||||
if frames_per_logical_second not in [30, 60]:
|
||||
drop_frame = False
|
||||
|
||||
raw_frames = hh * 3600 * frames_per_logical_second + mm * 60 * frames_per_logical_second + \
|
||||
ss * frames_per_logical_second + ff
|
||||
raw_frames = (
|
||||
hh * 3600 * frames_per_logical_second
|
||||
+ mm * 60 * frames_per_logical_second
|
||||
+ ss * frames_per_logical_second
|
||||
+ ff
|
||||
)
|
||||
|
||||
frames = raw_frames
|
||||
if drop_frame is True:
|
||||
frames_dropped_per_inst = (frames_per_logical_second / 15)
|
||||
frames_dropped_per_inst = frames_per_logical_second / 15
|
||||
mins = hh * 60 + mm
|
||||
inst_count = mins - math.floor(mins / 10)
|
||||
dropped_frames = int(frames_dropped_per_inst) * inst_count
|
||||
@@ -68,8 +82,12 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int,
|
||||
return frames
|
||||
|
||||
|
||||
def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, drop_frame: bool = False,
|
||||
fractional_frame: Optional[float] = None) -> str:
|
||||
def frame_count_to_smpte(
|
||||
frame_count: int,
|
||||
frames_per_logical_second: int,
|
||||
drop_frame: bool = False,
|
||||
fractional_frame: float | None = None,
|
||||
) -> str:
|
||||
assert frames_per_logical_second in [24, 25, 30, 48, 50, 60]
|
||||
assert fractional_frame is None or fractional_frame < 1.0
|
||||
|
||||
@@ -78,7 +96,7 @@ def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, drop_
|
||||
if drop_frame:
|
||||
assert frames_per_logical_second in [30, 60]
|
||||
mins, _ = divmod(nominal_frames, frames_per_logical_second * 60)
|
||||
frames_dropped_per_inst = (frames_per_logical_second / 15)
|
||||
frames_dropped_per_inst = frames_per_logical_second / 15
|
||||
inst_count = mins - math.floor(mins / 10)
|
||||
dropped_frames = frames_dropped_per_inst * inst_count
|
||||
nominal_frames = nominal_frames + dropped_frames
|
||||
@@ -89,14 +107,22 @@ def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, drop_
|
||||
ss, ff = divmod(rem, frames_per_logical_second)
|
||||
|
||||
hh = hh % 24
|
||||
|
||||
hh = int(hh)
|
||||
mm = int(mm)
|
||||
ss = int(ss)
|
||||
ff = int(ff)
|
||||
|
||||
if fractional_frame is not None and fractional_frame > 0:
|
||||
return "%02i:%02i:%02i%s%02i%s" % (hh, mm, ss, separator, ff, ("%.3f" % fractional_frame)[1:])
|
||||
fpart = "{fractional_frame:.3f}"[1:]
|
||||
return f"{hh:02}:{mm:02}:{ss:02}{separator}{ff:02}{fpart}"
|
||||
else:
|
||||
return "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff)
|
||||
return f"{hh:02}:{mm:02}:{ss:02}{separator}{ff:02}"
|
||||
# "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff)
|
||||
|
||||
|
||||
def footage_to_frame_count(footage_string) -> Optional[int]:
|
||||
m = re.search(r'(\d+)\+(\d+)(\.\d+)?', footage_string)
|
||||
def footage_to_frame_count(footage_string) -> int | None:
|
||||
m = re.search(r"(\d+)\+(\d+)(\.\d+)?", footage_string)
|
||||
if m is None:
|
||||
return None
|
||||
feet, frm, frac = m.groups()
|
||||
@@ -109,4 +135,4 @@ def footage_to_frame_count(footage_string) -> Optional[int]:
|
||||
|
||||
def frame_count_to_footage(frame_count):
|
||||
feet, frm = divmod(frame_count, 16)
|
||||
return "%i+%02i" % (feet, frm)
|
||||
return f"{feet}+{frm:02}"
|
||||
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
This module provides the main input document parsing and transform
|
||||
implementation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Iterator
|
||||
from fractions import Fraction
|
||||
from itertools import chain
|
||||
from json import JSONEncoder
|
||||
|
||||
import ptsl
|
||||
|
||||
from ptulsconv.broadcast_timecode import TimecodeFormat
|
||||
from ptulsconv.docparser import parse_document
|
||||
from ptulsconv.docparser.tag_compiler import TagCompiler
|
||||
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 .docparser.adr_entity import ADRLine, make_entities
|
||||
from .reporting import print_section_header_style, print_status_style, print_warning
|
||||
from .validations import (
|
||||
validate_dependent_value,
|
||||
validate_non_empty_field,
|
||||
validate_unique_field,
|
||||
)
|
||||
|
||||
|
||||
class FractionEncoder(JSONEncoder):
|
||||
"""
|
||||
A subclass of :class:`JSONEncoder` which encodes :class:`Fraction` objects
|
||||
as a dict.
|
||||
"""
|
||||
|
||||
force_denominator: int | None
|
||||
|
||||
def default(self, o):
|
||||
if isinstance(o, Fraction):
|
||||
return {"numerator": o.numerator, "denominator": o.denominator}
|
||||
else:
|
||||
return o.__dict__
|
||||
|
||||
|
||||
def output_adr_csv(lines: list[ADRLine], time_format: TimecodeFormat):
|
||||
"""
|
||||
Writes ADR lines as CSV to the current working directory. Creates
|
||||
directories for each character number and name pair, and within that
|
||||
directory, creates a CSV file for each reel.
|
||||
"""
|
||||
|
||||
reels: set[str | None] = {ln.reel for ln in lines}
|
||||
for n, name in [(n.character_id, n.character_name) for n in lines]:
|
||||
dir_name = f"{n}_{name}"
|
||||
os.makedirs(dir_name, exist_ok=True)
|
||||
os.chdir(dir_name)
|
||||
for reel in reels:
|
||||
these_lines = [
|
||||
ln for ln in lines if ln.character_id == n and ln.reel == reel
|
||||
]
|
||||
|
||||
if len(these_lines) == 0:
|
||||
continue
|
||||
|
||||
outfile_name = (
|
||||
f"{these_lines[0].title}_{n}_{these_lines[0].character_name}_{reel}.csv"
|
||||
)
|
||||
|
||||
with open(outfile_name, mode="w", newline="") as outfile:
|
||||
writer = csv.writer(outfile, dialect="excel")
|
||||
writer.writerow(
|
||||
[
|
||||
"Title",
|
||||
"Character Name",
|
||||
"Cue Number",
|
||||
"Reel",
|
||||
"Version",
|
||||
"Start",
|
||||
"Finish",
|
||||
"Start Seconds",
|
||||
"Finish Seconds",
|
||||
"Prompt",
|
||||
"Reason",
|
||||
"Note",
|
||||
"TV",
|
||||
]
|
||||
)
|
||||
|
||||
for event in these_lines:
|
||||
this_start = event.start or 0
|
||||
this_finish = event.finish or 0
|
||||
this_row = [
|
||||
event.title,
|
||||
event.character_name,
|
||||
event.cue_number,
|
||||
event.reel,
|
||||
event.version,
|
||||
time_format.seconds_to_smpte(this_start),
|
||||
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)
|
||||
os.chdir("..")
|
||||
|
||||
|
||||
def generate_documents(session_tc_format, scenes, adr_lines: list[ADRLine], title):
|
||||
"""
|
||||
Create PDF output.
|
||||
"""
|
||||
print_section_header_style("Creating PDF Reports")
|
||||
report_date = datetime.datetime.now()
|
||||
reports_dir = f"{title}_{report_date.strftime('%Y-%m-%d_%H%M%S')}"
|
||||
os.makedirs(reports_dir, exist_ok=False)
|
||||
os.chdir(reports_dir)
|
||||
|
||||
client = next((x.client for x in adr_lines), "")
|
||||
supervisor = next((x.supervisor for x in adr_lines), "")
|
||||
|
||||
output_continuity(
|
||||
scenes=scenes,
|
||||
tc_display_format=session_tc_format,
|
||||
title=title,
|
||||
client=client or "",
|
||||
supervisor=supervisor,
|
||||
)
|
||||
|
||||
reels = ["R1", "R2", "R3", "R4", "R5", "R6"]
|
||||
|
||||
if len(adr_lines) == 0:
|
||||
print_status_style(
|
||||
"No ADR lines were found in the input document. "
|
||||
"ADR reports will not be generated."
|
||||
)
|
||||
|
||||
else:
|
||||
create_adr_reports(
|
||||
adr_lines, tc_display_format=session_tc_format, reel_list=sorted(reels)
|
||||
)
|
||||
|
||||
|
||||
def create_adr_reports(
|
||||
lines: list[ADRLine], tc_display_format: TimecodeFormat, reel_list: list[str]
|
||||
):
|
||||
"""
|
||||
Creates a directory heirarchy and a respective set of ADR reports,
|
||||
given a list of lines.
|
||||
"""
|
||||
|
||||
print_status_style("Creating ADR Report")
|
||||
output_summary(lines, tc_display_format=tc_display_format)
|
||||
|
||||
print_status_style("Creating Line Count")
|
||||
output_line_count(lines, reel_list=reel_list)
|
||||
|
||||
print_status_style("Creating Supervisor Logs directory and reports")
|
||||
os.makedirs("Supervisor Logs", exist_ok=True)
|
||||
os.chdir("Supervisor Logs")
|
||||
output_supervisor_1pg(lines, tc_display_format=tc_display_format)
|
||||
os.chdir("..")
|
||||
|
||||
print_status_style("Creating Director's Logs director and reports")
|
||||
os.makedirs("Director Logs", exist_ok=True)
|
||||
os.chdir("Director Logs")
|
||||
output_summary(lines, tc_display_format=tc_display_format, by_character=True)
|
||||
os.chdir("..")
|
||||
|
||||
print_status_style("Creating CSV outputs")
|
||||
os.makedirs("CSV", exist_ok=True)
|
||||
os.chdir("CSV")
|
||||
output_adr_csv(lines, time_format=tc_display_format)
|
||||
os.chdir("..")
|
||||
|
||||
print_status_style("Creating Scripts directory and reports")
|
||||
os.makedirs("Talent Scripts", exist_ok=True)
|
||||
os.chdir("Talent Scripts")
|
||||
output_talent_sides(lines, tc_display_format=tc_display_format)
|
||||
|
||||
|
||||
def convert(major_mode, input_file=None, output=sys.stdout, warnings=True):
|
||||
"""
|
||||
Primary worker function, accepts the input file and decides
|
||||
what to do with it based on the `major_mode`.
|
||||
|
||||
:param input_file: a path to the input file.
|
||||
:param major_mode: the selected output mode, 'raw', 'tagged' or 'doc'.
|
||||
"""
|
||||
session_text = ""
|
||||
if input_file is not None:
|
||||
with open(input_file, "r") as file:
|
||||
session_text = file.read()
|
||||
else:
|
||||
with ptsl.open_engine(
|
||||
company_name="The ptulsconv developers", application_name="ptulsconv"
|
||||
) as engine:
|
||||
req = engine.export_session_as_text()
|
||||
req.utf8_encoding()
|
||||
req.include_track_edls()
|
||||
req.include_markers()
|
||||
req.time_type("tc")
|
||||
req.dont_show_crossfades()
|
||||
req.selected_tracks_only()
|
||||
session_text = req.export_string()
|
||||
|
||||
session = parse_document(session_text)
|
||||
session_tc_format = session.header.timecode_format
|
||||
|
||||
if major_mode == "raw":
|
||||
output.write(FractionEncoder().encode(session))
|
||||
|
||||
else:
|
||||
compiler = TagCompiler()
|
||||
compiler.session = session
|
||||
compiled_events = list(compiler.compile_events())
|
||||
|
||||
if major_mode == "tagged":
|
||||
output.write(FractionEncoder().encode(compiled_events))
|
||||
|
||||
elif major_mode == "doc":
|
||||
generic_events, adr_lines = make_entities(compiled_events)
|
||||
|
||||
scenes = sorted(
|
||||
[s for s in compiler.compile_all_time_spans() if s[0] == "Sc"],
|
||||
key=lambda x: x[2],
|
||||
)
|
||||
|
||||
# TODO: Breakdown by titles
|
||||
titles = {x.title for x in (generic_events + adr_lines)}
|
||||
if len(titles) != 1:
|
||||
print_warning(
|
||||
"Multiple titles per export is not supported, "
|
||||
f"found multiple titles: {titles} Exiting."
|
||||
)
|
||||
sys.exit(-1)
|
||||
|
||||
title = next(iter(titles))
|
||||
|
||||
print_status_style(f"{len(generic_events)} generic events found.")
|
||||
print_status_style(f"{len(adr_lines)} ADR events found.")
|
||||
|
||||
if warnings:
|
||||
perform_adr_validations(iter(adr_lines))
|
||||
|
||||
generate_documents(session_tc_format, scenes, adr_lines, title)
|
||||
|
||||
|
||||
def perform_adr_validations(lines: Iterator[ADRLine]):
|
||||
"""
|
||||
Performs validations on the input.
|
||||
"""
|
||||
for warning in chain(
|
||||
validate_unique_field(lines, field="cue_number", scope="title"),
|
||||
validate_non_empty_field(lines, field="cue_number"),
|
||||
validate_non_empty_field(lines, field="character_id"),
|
||||
validate_non_empty_field(lines, field="title"),
|
||||
validate_dependent_value(
|
||||
lines, key_field="character_id", dependent_field="character_name"
|
||||
),
|
||||
validate_dependent_value(
|
||||
lines, key_field="character_id", dependent_field="actor_name"
|
||||
),
|
||||
):
|
||||
print_warning(warning.report_message())
|
||||
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Docparser module
|
||||
"""
|
||||
|
||||
from .pt_doc_parser import parse_document # pyright: ignore[reportUnusedImport, reportUnusedImport] # noqa: F401, I001
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
# from dataclasses import dataclass
|
||||
@@ -0,0 +1,328 @@
|
||||
from parsimonious.grammar import Grammar
|
||||
from parsimonious.nodes import NodeVisitor
|
||||
|
||||
from .doc_entity import (
|
||||
ClipDescriptor,
|
||||
FileDescriptor,
|
||||
HeaderDescriptor,
|
||||
MarkerDescriptor,
|
||||
PluginDescriptor,
|
||||
SessionDescriptor,
|
||||
TrackClipDescriptor,
|
||||
TrackDescriptor,
|
||||
)
|
||||
|
||||
protools_text_export_grammar = Grammar(
|
||||
r"""
|
||||
document = header files_section? clips_section? plugin_listing?
|
||||
track_listing? markers_block?
|
||||
|
||||
header = "SESSION NAME:" fs string_value rs
|
||||
"SAMPLE RATE:" fs float_value rs
|
||||
"BIT DEPTH:" fs integer_value "-bit" rs
|
||||
"SESSION START TIMECODE:" fs string_value rs
|
||||
"TIMECODE FORMAT:" fs frame_rate " Drop"? " Frame" rs
|
||||
"# OF AUDIO TRACKS:" fs integer_value rs
|
||||
"# OF AUDIO CLIPS:" fs integer_value rs
|
||||
"# OF AUDIO FILES:" fs integer_value rs block_ending
|
||||
|
||||
frame_rate = ("60" / "59.94" / "30" / "29.97" / "25" / "24" /
|
||||
"23.976")
|
||||
files_section = files_header files_column_header file_record*
|
||||
block_ending
|
||||
files_header = "F I L E S I N S E S S I O N" rs
|
||||
files_column_header = "Filename" isp fs "Location" rs
|
||||
file_record = string_value fs string_value rs
|
||||
|
||||
clips_section = clips_header clips_column_header clip_record*
|
||||
block_ending
|
||||
clips_header = "O N L I N E C L I P S I N S E S S I O N" rs
|
||||
clips_column_header = string_value fs string_value rs
|
||||
clip_record = string_value fs string_value
|
||||
(fs "[" integer_value "]")? rs
|
||||
|
||||
plugin_listing = plugin_header plugin_column_header plugin_record*
|
||||
block_ending
|
||||
plugin_header = "P L U G - I N S L I S T I N G" rs
|
||||
plugin_column_header = "MANUFACTURER " fs
|
||||
"PLUG-IN NAME " fs
|
||||
"VERSION " fs
|
||||
"FORMAT " fs
|
||||
"STEMS " fs
|
||||
"NUMBER OF INSTANCES" rs
|
||||
plugin_record = string_value fs string_value fs string_value fs
|
||||
string_value fs string_value fs string_value rs
|
||||
|
||||
track_listing = track_listing_header track_block*
|
||||
track_block = track_list_top ( track_clip_entry / block_ending )*
|
||||
|
||||
track_listing_header = "T R A C K L I S T I N G" rs
|
||||
track_list_top = "TRACK NAME:" fs string_value rs
|
||||
"COMMENTS:" fs string_value rs
|
||||
"USER DELAY:" fs integer_value " Samples" rs
|
||||
"STATE: " track_state_list rs
|
||||
("PLUG-INS: " ( fs string_value )* rs)?
|
||||
"CHANNEL " fs "EVENT " fs
|
||||
"CLIP NAME " fs
|
||||
"START TIME " fs "END TIME " fs
|
||||
"DURATION " fs
|
||||
("TIMESTAMP " fs)? "STATE" rs
|
||||
|
||||
track_state_list = (track_state " ")*
|
||||
|
||||
track_state = "Solo" / "Muted" / "Inactive" / "Hidden"
|
||||
|
||||
track_clip_entry = integer_value isp fs
|
||||
integer_value isp fs
|
||||
string_value fs
|
||||
string_value fs string_value fs string_value fs
|
||||
(string_value fs)?
|
||||
track_clip_state rs
|
||||
|
||||
track_clip_state = ("Muted" / "Unmuted")
|
||||
|
||||
markers_block = markers_block_header
|
||||
(markers_list / markers_list_simple)
|
||||
|
||||
markers_list_simple = markers_column_header_simple marker_record_simple*
|
||||
|
||||
markers_list = markers_column_header marker_record*
|
||||
|
||||
markers_block_header = "M A R K E R S L I S T I N G" rs
|
||||
|
||||
markers_column_header_simple =
|
||||
"# LOCATION TIME REFERENCE "
|
||||
"UNITS NAME "
|
||||
"COMMENTS" rs
|
||||
|
||||
markers_column_header =
|
||||
"# LOCATION TIME REFERENCE "
|
||||
"UNITS NAME "
|
||||
"TRACK NAME "
|
||||
"TRACK TYPE COMMENTS" rs
|
||||
|
||||
marker_record_simple = integer_value isp fs string_value fs
|
||||
integer_value isp fs string_value fs string_value
|
||||
fs string_value rs
|
||||
|
||||
marker_record = integer_value isp fs string_value fs integer_value isp fs
|
||||
string_value fs string_value fs string_value fs
|
||||
string_value fs string_value rs
|
||||
|
||||
fs = "\t"
|
||||
rs = "\n"
|
||||
block_ending = rs rs
|
||||
string_value = ~r"[^\t\n]*"
|
||||
integer_value = ~r"\d+"
|
||||
float_value = ~r"\d+(\.\d+)?"
|
||||
isp = ~r"[^\d\t\n]*"
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def parse_document(session_text: str) -> SessionDescriptor:
|
||||
"""
|
||||
Parse a Pro Tools text export.
|
||||
:param session_text: Pro Tools session text export
|
||||
:return: the session descriptor
|
||||
"""
|
||||
ast = protools_text_export_grammar.parse(session_text)
|
||||
return DocParserVisitor().visit(ast)
|
||||
|
||||
|
||||
class DocParserVisitor(NodeVisitor):
|
||||
def __init__(self):
|
||||
self.track_index = 0
|
||||
|
||||
# @staticmethod
|
||||
def visit_document(self, _, visited_children) -> SessionDescriptor:
|
||||
self.track_index = 0
|
||||
files = next(iter(visited_children[1]), None)
|
||||
clips = next(iter(visited_children[2]), None)
|
||||
plugins = next(iter(visited_children[3]), None)
|
||||
tracks = next(iter(visited_children[4]), None)
|
||||
markers = next(iter(visited_children[5]), None)
|
||||
|
||||
return SessionDescriptor(
|
||||
header=visited_children[0],
|
||||
files=files,
|
||||
clips=clips,
|
||||
plugins=plugins,
|
||||
tracks=tracks,
|
||||
markers=markers,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def visit_header(_, visited_children):
|
||||
|
||||
tc_drop = False
|
||||
for _ in visited_children[20]:
|
||||
tc_drop = True
|
||||
|
||||
return HeaderDescriptor(
|
||||
session_name=visited_children[2],
|
||||
sample_rate=visited_children[6],
|
||||
bit_depth=visited_children[10],
|
||||
start_timecode=visited_children[15],
|
||||
timecode_format=visited_children[19],
|
||||
timecode_drop_frame=tc_drop,
|
||||
count_audio_tracks=visited_children[25],
|
||||
count_clips=visited_children[29],
|
||||
count_files=visited_children[33],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def visit_files_section(_, visited_children):
|
||||
return [
|
||||
FileDescriptor(filemname=child[0], path=child[2])
|
||||
for child in visited_children[2]
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def visit_clips_section(_, visited_children):
|
||||
channel = next(iter(visited_children[2][3]), 1)
|
||||
return [
|
||||
ClipDescriptor(clip_name=child[0], file=child[2], channel=channel)
|
||||
for child in visited_children[2]
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def visit_plugin_listing(_, visited_children):
|
||||
|
||||
return [
|
||||
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
|
||||
clips = []
|
||||
for clip in track_clip_list:
|
||||
if clip[0] is not None:
|
||||
clips.append(clip[0])
|
||||
|
||||
plugins = []
|
||||
for plugin_opt in track_header[16]:
|
||||
for plugin in plugin_opt[1]:
|
||||
plugins.append(plugin[1])
|
||||
|
||||
this_index = self.track_index
|
||||
self.track_index += 1
|
||||
|
||||
return TrackDescriptor(
|
||||
index=this_index,
|
||||
name=track_header[2],
|
||||
comments=track_header[6],
|
||||
user_delay_samples=track_header[10],
|
||||
state=track_header[14],
|
||||
plugins=plugins,
|
||||
clips=clips,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def visit_frame_rate(node, _):
|
||||
return node.text
|
||||
|
||||
@staticmethod
|
||||
def visit_track_listing(_, visited_children):
|
||||
return visited_children[1]
|
||||
|
||||
@staticmethod
|
||||
def visit_track_clip_entry(_, visited_children):
|
||||
timestamp = None
|
||||
if isinstance(visited_children[14], list):
|
||||
timestamp = visited_children[14][0][0]
|
||||
|
||||
return TrackClipDescriptor(
|
||||
channel=visited_children[0],
|
||||
event=visited_children[3],
|
||||
clip_name=visited_children[6],
|
||||
start_time=visited_children[8],
|
||||
finish_time=visited_children[10],
|
||||
duration=visited_children[12],
|
||||
timestamp=timestamp,
|
||||
state=visited_children[15],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def visit_track_state_list(_, visited_children):
|
||||
states = []
|
||||
for next_state in visited_children:
|
||||
states.append(next_state[0][0].text)
|
||||
return states
|
||||
|
||||
@staticmethod
|
||||
def visit_track_clip_state(node, _):
|
||||
return node.text
|
||||
|
||||
@staticmethod
|
||||
def visit_markers_block(_, visited_children):
|
||||
markers = []
|
||||
|
||||
for marker in visited_children[1][0][1]:
|
||||
marker: MarkerDescriptor
|
||||
markers.append(marker)
|
||||
|
||||
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
|
||||
def visit_marker_record(_, visited_children):
|
||||
track_type = visited_children[15]
|
||||
is_track_marker = track_type == "Track"
|
||||
|
||||
return MarkerDescriptor(
|
||||
number=visited_children[0],
|
||||
location=visited_children[3],
|
||||
time_reference=visited_children[5],
|
||||
units=visited_children[8],
|
||||
name=visited_children[10],
|
||||
comments=visited_children[16],
|
||||
track_marker=is_track_marker,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def visit_formatted_clip_name(_, visited_children):
|
||||
return visited_children[1].text
|
||||
|
||||
@staticmethod
|
||||
def visit_string_value(node, _):
|
||||
return node.text.strip(" ")
|
||||
|
||||
@staticmethod
|
||||
def visit_integer_value(node, _):
|
||||
return int(node.text)
|
||||
|
||||
# def visit_timecode_value(self, node, visited_children):
|
||||
# return node.text.strip(" ")
|
||||
|
||||
@staticmethod
|
||||
def visit_float_value(node, _):
|
||||
return float(node.text)
|
||||
|
||||
def visit_block_ending(self, node, visited_children):
|
||||
pass
|
||||
|
||||
def generic_visit(self, node, visited_children):
|
||||
"""The generic visit method."""
|
||||
return visited_children or node
|
||||
@@ -1,11 +1,13 @@
|
||||
from collections import namedtuple
|
||||
from fractions import Fraction
|
||||
from typing import Iterator, Tuple, Callable, Generator, Dict, List
|
||||
|
||||
import ptulsconv.docparser.doc_entity as doc_entity
|
||||
from .tagged_string_parser_visitor import parse_tags, TagPreModes
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator, Iterator
|
||||
from dataclasses import dataclass
|
||||
from fractions import Fraction
|
||||
from typing import Callable, NamedTuple
|
||||
|
||||
from ptulsconv.docparser import doc_entity
|
||||
|
||||
from .tagged_string_parser_visitor import TagPreModes, parse_tags
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -13,7 +15,7 @@ class Event:
|
||||
clip_name: str
|
||||
track_name: str
|
||||
session_name: str
|
||||
tags: Dict[str, str]
|
||||
tags: dict[str, str]
|
||||
start: Fraction
|
||||
finish: Fraction
|
||||
|
||||
@@ -24,31 +26,41 @@ class TagCompiler:
|
||||
items.
|
||||
"""
|
||||
|
||||
Intermediate = namedtuple('Intermediate', 'track_content track_tags track_comment_tags '
|
||||
'clip_content clip_tags clip_tag_mode start finish')
|
||||
class Intermediate(NamedTuple):
|
||||
track_content: str
|
||||
clip_content: str
|
||||
track_tags: dict[str, str]
|
||||
track_comment_tags: dict[str, str]
|
||||
clip_tags: dict[str, str]
|
||||
clip_tag_mode: TagPreModes
|
||||
start: Fraction
|
||||
finish: Fraction
|
||||
|
||||
session: doc_entity.SessionDescriptor
|
||||
|
||||
def compile_all_time_spans(self) -> List[Tuple[str, str, Fraction, Fraction]]:
|
||||
def compile_all_time_spans(self) -> list[tuple[str, str, Fraction, Fraction]]:
|
||||
"""
|
||||
:returns: A `List` of (key: str, value: str, start: Fraction, finish: Fraction)
|
||||
:returns: A `List` of (key: str, value: str, start: Fraction,
|
||||
finish: Fraction)
|
||||
"""
|
||||
ret_list = list()
|
||||
ret_list = []
|
||||
for element in self.parse_data():
|
||||
if element.clip_tag_mode == TagPreModes.TIMESPAN:
|
||||
for k in element.clip_tags.keys():
|
||||
ret_list.append((k, element.clip_tags[k], element.start, element.finish))
|
||||
for k in element.clip_tags:
|
||||
ret_list.append(
|
||||
(k, element.clip_tags[k], element.start, element.finish)
|
||||
)
|
||||
|
||||
return ret_list
|
||||
|
||||
def compile_tag_list(self) -> Dict[str, List[str]]:
|
||||
tags_dict = dict()
|
||||
def compile_tag_list(self) -> dict[str, list[str]]:
|
||||
tags_dict = {}
|
||||
|
||||
def update_tags_dict(other_dict: dict):
|
||||
for k in other_dict.keys():
|
||||
if k not in tags_dict.keys():
|
||||
for k, value in other_dict.items():
|
||||
if k not in tags_dict:
|
||||
tags_dict[k] = set()
|
||||
tags_dict[k].add(other_dict[k])
|
||||
tags_dict[k].add(value)
|
||||
|
||||
for parsed in self.parse_data():
|
||||
update_tags_dict(parsed.clip_tags)
|
||||
@@ -73,32 +85,45 @@ class TagCompiler:
|
||||
step3 = self.collect_time_spans(step2)
|
||||
step4 = self.apply_tags(step3)
|
||||
for datum in step4:
|
||||
yield Event(clip_name=datum[0], track_name=datum[1], session_name=datum[2],
|
||||
tags=datum[3], start=datum[4], finish=datum[5])
|
||||
yield Event(
|
||||
clip_name=datum[0],
|
||||
track_name=datum[1],
|
||||
session_name=datum[2],
|
||||
tags=datum[3],
|
||||
start=datum[4],
|
||||
finish=datum[5],
|
||||
)
|
||||
|
||||
def _marker_tags(self, at):
|
||||
retval = dict()
|
||||
retval = {}
|
||||
|
||||
applicable = [(m, t) for (m, t) in self.session.markers_timed() if t <= at]
|
||||
|
||||
for marker, _ in sorted(applicable, key=lambda x: x[1]):
|
||||
retval.update(parse_tags(marker.comments or "").tag_dict)
|
||||
retval.update(parse_tags(marker.name or "").tag_dict)
|
||||
|
||||
return retval
|
||||
|
||||
def filter_out_directives(self, clips : Iterator[Intermediate]) -> Iterator[Intermediate]:
|
||||
def filter_out_directives(
|
||||
self, clips: Iterator[Intermediate]
|
||||
) -> Iterator[Intermediate]:
|
||||
for clip in clips:
|
||||
if clip.clip_tag_mode == 'Directive':
|
||||
if clip.clip_tag_mode == "Directive":
|
||||
continue
|
||||
else:
|
||||
yield clip
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _coalesce_tags(clip_tags: dict, track_tags: dict,
|
||||
def _coalesce_tags(
|
||||
clip_tags: dict,
|
||||
track_tags: dict,
|
||||
track_comment_tags: dict,
|
||||
timespan_tags: dict,
|
||||
marker_tags: dict, session_tags: dict):
|
||||
effective_tags = dict()
|
||||
marker_tags: dict,
|
||||
session_tags: dict,
|
||||
):
|
||||
effective_tags = {}
|
||||
effective_tags.update(session_tags)
|
||||
effective_tags.update(marker_tags)
|
||||
effective_tags.update(timespan_tags)
|
||||
@@ -108,82 +133,99 @@ class TagCompiler:
|
||||
return effective_tags
|
||||
|
||||
def parse_data(self) -> Iterator[Intermediate]:
|
||||
|
||||
for track, clip, start, finish, _ in self.session.track_clips_timed():
|
||||
if clip.state == 'Muted':
|
||||
if clip.state == "Muted":
|
||||
continue
|
||||
|
||||
track_parsed = parse_tags(track.name)
|
||||
track_comments_parsed = parse_tags(track.comments)
|
||||
clip_parsed = parse_tags(clip.clip_name)
|
||||
|
||||
yield TagCompiler.Intermediate(track_content=track_parsed.content,
|
||||
yield TagCompiler.Intermediate(
|
||||
track_content=track_parsed.content,
|
||||
track_tags=track_parsed.tag_dict,
|
||||
track_comment_tags=track_comments_parsed.tag_dict,
|
||||
clip_content=clip_parsed.content,
|
||||
clip_tags=clip_parsed.tag_dict,
|
||||
clip_tag_mode=clip_parsed.mode,
|
||||
start=start, finish=finish)
|
||||
start=start,
|
||||
finish=finish,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def apply_appends(parsed: Iterator[Intermediate]) -> Iterator[Intermediate]:
|
||||
|
||||
def should_append(a, b):
|
||||
return b.clip_tag_mode == TagPreModes.APPEND and b.start >= a.finish
|
||||
|
||||
def do_append(a, b):
|
||||
merged_tags = dict(a.clip_tags)
|
||||
merged_tags.update(b.clip_tags)
|
||||
return TagCompiler.Intermediate(track_content=a.track_content,
|
||||
return TagCompiler.Intermediate(
|
||||
track_content=a.track_content,
|
||||
track_tags=a.track_tags,
|
||||
track_comment_tags=a.track_comment_tags,
|
||||
clip_content=a.clip_content + ' ' + b.clip_content,
|
||||
clip_tags=merged_tags, clip_tag_mode=a.clip_tag_mode,
|
||||
start=a.start, finish=b.finish)
|
||||
clip_content=a.clip_content + " " + b.clip_content,
|
||||
clip_tags=merged_tags,
|
||||
clip_tag_mode=a.clip_tag_mode,
|
||||
start=a.start,
|
||||
finish=b.finish,
|
||||
)
|
||||
|
||||
yield from apply_appends(parsed, should_append, do_append)
|
||||
|
||||
@staticmethod
|
||||
def collect_time_spans(parsed: Iterator[Intermediate]) -> \
|
||||
Iterator[Tuple[Intermediate, Tuple[dict, Fraction, Fraction]]]:
|
||||
|
||||
time_spans = list()
|
||||
def collect_time_spans(
|
||||
parsed: Iterator[Intermediate],
|
||||
) -> Iterator[tuple[Intermediate, tuple[dict, Fraction, Fraction]]]:
|
||||
time_spans = []
|
||||
|
||||
for item in parsed:
|
||||
if item.clip_tag_mode == TagPreModes.TIMESPAN:
|
||||
time_spans.append((item.clip_tags, item.start, item.finish))
|
||||
else:
|
||||
yield item, list(time_spans)
|
||||
yield item, tuple(time_spans)
|
||||
|
||||
@staticmethod
|
||||
def _time_span_tags(at_time: Fraction, applicable_spans) -> dict:
|
||||
retval = dict()
|
||||
for tags in reversed([a[0] for a in applicable_spans if a[1] <= at_time <= a[2]]):
|
||||
retval = {}
|
||||
for tags in reversed(
|
||||
[a[0] for a in applicable_spans if a[1] <= at_time <= a[2]]
|
||||
):
|
||||
retval.update(tags)
|
||||
|
||||
return retval
|
||||
|
||||
def apply_tags(self, parsed_with_time_spans) -> Iterator[Tuple[str, str, str, dict, Fraction, Fraction]]:
|
||||
|
||||
def apply_tags(
|
||||
self, parsed_with_time_spans
|
||||
) -> Iterator[tuple[str, str, str, dict, Fraction, Fraction]]:
|
||||
session_parsed = parse_tags(self.session.header.session_name)
|
||||
|
||||
for event, time_spans in parsed_with_time_spans:
|
||||
event: 'TagCompiler.Intermediate'
|
||||
event: TagCompiler.Intermediate
|
||||
marker_tags = self._marker_tags(event.start)
|
||||
time_span_tags = self._time_span_tags(event.start, time_spans)
|
||||
tags = self._coalesce_tags(clip_tags=event.clip_tags,
|
||||
tags = self._coalesce_tags(
|
||||
clip_tags=event.clip_tags,
|
||||
track_tags=event.track_tags,
|
||||
track_comment_tags=event.track_comment_tags,
|
||||
timespan_tags=time_span_tags,
|
||||
marker_tags=marker_tags,
|
||||
session_tags=session_parsed.tag_dict)
|
||||
session_tags=session_parsed.tag_dict,
|
||||
)
|
||||
|
||||
yield event.clip_content, event.track_content, session_parsed.content, tags, event.start, event.finish
|
||||
yield (
|
||||
event.clip_content,
|
||||
event.track_content,
|
||||
session_parsed.content,
|
||||
tags,
|
||||
event.start,
|
||||
event.finish,
|
||||
)
|
||||
|
||||
|
||||
def apply_appends(source: Iterator,
|
||||
should_append: Callable,
|
||||
do_append: Callable) -> Generator:
|
||||
def apply_appends(
|
||||
source: Iterator, should_append: Callable, preform_append: Callable
|
||||
) -> Generator:
|
||||
"""
|
||||
:param source:
|
||||
:param should_append: Called with two variables a and b, your
|
||||
@@ -196,7 +238,7 @@ def apply_appends(source: Iterator,
|
||||
this_element = next(source)
|
||||
for element in source:
|
||||
if should_append(this_element, element):
|
||||
this_element = do_append(this_element, element)
|
||||
this_element = preform_append(this_element, element)
|
||||
else:
|
||||
yield this_element
|
||||
this_element = element
|
||||
@@ -1,48 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Optional, Callable, Any, List
|
||||
from typing import Any, Callable
|
||||
|
||||
# from ptulsconv.docparser.adr_entity import GenericEvent
|
||||
|
||||
|
||||
class TagMapping:
|
||||
class ContentSource(Enum):
|
||||
Session = 1,
|
||||
Track = 2,
|
||||
Clip = 3,
|
||||
Session = (1,)
|
||||
Track = (2,)
|
||||
Clip = (3,)
|
||||
|
||||
source: str
|
||||
alternate_source: Optional[ContentSource]
|
||||
alternate_source: ContentSource | None
|
||||
formatter: Callable[[str], Any]
|
||||
|
||||
@staticmethod
|
||||
def print_rules(for_type: object, output=sys.stdout):
|
||||
format_str = "%-20s | %-20s | %-25s"
|
||||
hr = "%s+%s+%s" % ("-" * 21, "-" * 23, "-" * 26)
|
||||
print("Tag mapping for %s" % for_type.__name__)
|
||||
hr = f"{'-' * 21}+{'-' * 23}+{'-' * 26}"
|
||||
print(f"Tag mapping for {for_type.__class__}")
|
||||
print(hr)
|
||||
print(format_str % ("Tag Source", "Target", "Type"),
|
||||
file=output)
|
||||
print(format_str % ("Tag Source", "Target", "Type"), file=output)
|
||||
print(hr)
|
||||
for rule in for_type.tag_mapping:
|
||||
t = for_type.__annotations__[rule.target]
|
||||
print(format_str % (rule.source, rule.target, t),
|
||||
file=output)
|
||||
print(format_str % (rule.source, rule.target, t), file=output)
|
||||
if rule.alternate_source is TagMapping.ContentSource.Session:
|
||||
print(format_str % (" - (Session Name)", rule.target, t),
|
||||
file=output)
|
||||
print(format_str % (" - (Session Name)", rule.target, t), file=output)
|
||||
elif rule.alternate_source is TagMapping.ContentSource.Track:
|
||||
print(format_str % (" - (Track Name)", rule.target, t),
|
||||
file=output)
|
||||
print(format_str % (" - (Track Name)", rule.target, t), file=output)
|
||||
elif rule.alternate_source is TagMapping.ContentSource.Clip:
|
||||
print(format_str % (" - (Clip Name)", rule.target, t),
|
||||
file=output)
|
||||
print(format_str % (" - (Clip Name)", rule.target, t), file=output)
|
||||
|
||||
@staticmethod
|
||||
def apply_rules(rules: List['TagMapping'],
|
||||
def apply_rules(
|
||||
rules: list[TagMapping],
|
||||
tags: dict,
|
||||
clip_content: str,
|
||||
track_content: str,
|
||||
session_content: str,
|
||||
to: object):
|
||||
to: object,
|
||||
):
|
||||
|
||||
done = set()
|
||||
for rule in rules:
|
||||
@@ -51,23 +52,30 @@ class TagMapping:
|
||||
if rule.apply(tags, clip_content, track_content, session_content, to):
|
||||
done.update(rule.target)
|
||||
|
||||
def __init__(self, source: str,
|
||||
def __init__(
|
||||
self,
|
||||
source: str,
|
||||
target: str,
|
||||
alt: Optional[ContentSource] = None,
|
||||
formatter=None):
|
||||
alt: ContentSource | None = None,
|
||||
formatter=None,
|
||||
):
|
||||
self.source = source
|
||||
self.target = target
|
||||
self.alternate_source = alt
|
||||
self.formatter = formatter or (lambda x: x)
|
||||
|
||||
def apply(self, tags: dict,
|
||||
def apply(
|
||||
self,
|
||||
tags: dict,
|
||||
clip_content: str,
|
||||
track_content: str,
|
||||
session_content: str, to: object) -> bool:
|
||||
session_content: str,
|
||||
to: object,
|
||||
) -> bool:
|
||||
|
||||
new_value = None
|
||||
|
||||
if self.source in tags.keys():
|
||||
if self.source in tags:
|
||||
new_value = tags[self.source]
|
||||
elif self.alternate_source == TagMapping.ContentSource.Session:
|
||||
new_value = session_content
|
||||
+15
-15
@@ -1,13 +1,13 @@
|
||||
from parsimonious import NodeVisitor, Grammar
|
||||
from typing import Dict, Union
|
||||
from enum import Enum
|
||||
|
||||
from parsimonious import Grammar, NodeVisitor
|
||||
|
||||
|
||||
class TagPreModes(Enum):
|
||||
NORMAL = 'Normal'
|
||||
APPEND = 'Append'
|
||||
TIMESPAN = 'Timespan'
|
||||
DIRECTIVE = 'Directive'
|
||||
NORMAL = "Normal"
|
||||
APPEND = "Append"
|
||||
TIMESPAN = "Timespan"
|
||||
DIRECTIVE = "Directive"
|
||||
|
||||
|
||||
tag_grammar = Grammar(
|
||||
@@ -36,7 +36,7 @@ def parse_tags(prompt: str) -> "TaggedStringResult":
|
||||
|
||||
class TaggedStringResult:
|
||||
content: str
|
||||
tag_dict: Dict[str, str]
|
||||
tag_dict: dict[str, str]
|
||||
mode: TagPreModes
|
||||
|
||||
def __init__(self, content, tag_dict, mode):
|
||||
@@ -46,14 +46,14 @@ class TaggedStringResult:
|
||||
|
||||
|
||||
class TagListVisitor(NodeVisitor):
|
||||
|
||||
@staticmethod
|
||||
def visit_document(_, visited_children) -> TaggedStringResult:
|
||||
modifier_opt, line_opt, _, tag_list_opt = visited_children
|
||||
|
||||
return TaggedStringResult(content=next(iter(line_opt), None),
|
||||
tag_dict=next(iter(tag_list_opt), dict()),
|
||||
mode=TagPreModes(next(iter(modifier_opt), 'Normal'))
|
||||
return TaggedStringResult(
|
||||
content=next(iter(line_opt), None),
|
||||
tag_dict=next(iter(tag_list_opt), {}),
|
||||
mode=TagPreModes(next(iter(modifier_opt), "Normal")),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -62,18 +62,18 @@ class TagListVisitor(NodeVisitor):
|
||||
|
||||
@staticmethod
|
||||
def visit_modifier(node, _):
|
||||
if node.text.startswith('@'):
|
||||
if node.text.startswith("@"):
|
||||
return TagPreModes.TIMESPAN
|
||||
elif node.text.startswith('&'):
|
||||
elif node.text.startswith("&"):
|
||||
return TagPreModes.APPEND
|
||||
elif node.text.startswith('!'):
|
||||
elif node.text.startswith("!"):
|
||||
return TagPreModes.DIRECTIVE
|
||||
else:
|
||||
return TagPreModes.NORMAL
|
||||
|
||||
@staticmethod
|
||||
def visit_tag_list(_, visited_children):
|
||||
retdict = dict()
|
||||
retdict = {}
|
||||
for child in visited_children:
|
||||
if child[0] is not None:
|
||||
k, v = child[0]
|
||||
@@ -1,12 +1,14 @@
|
||||
"""
|
||||
Methods for converting string reprentations of film footage.
|
||||
"""
|
||||
from fractions import Fraction
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Optional
|
||||
from fractions import Fraction
|
||||
|
||||
|
||||
def footage_to_seconds(footage: str) -> Optional[Fraction]:
|
||||
def footage_to_seconds(footage: str) -> Fraction | None:
|
||||
"""
|
||||
Converts a string representation of a footage (35mm, 24fps)
|
||||
into a :class:`Fraction`, this fraction being a some number of
|
||||
@@ -15,7 +17,7 @@ def footage_to_seconds(footage: str) -> Optional[Fraction]:
|
||||
:param footage: A string reprenentation of a footage of the form
|
||||
resembling "90+01".
|
||||
"""
|
||||
m = re.match(r'(\d+)\+(\d+)(\.\d+)?', footage)
|
||||
m = re.match(r"(\d+)\+(\d+)(\.\d+)?", footage)
|
||||
if m is None:
|
||||
return None
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
#import ffmpeg # ffmpeg-python
|
||||
# import ffmpeg # ffmpeg-python
|
||||
|
||||
# TODO: Implement movie export
|
||||
|
||||
# def create_movie(event):
|
||||
# start = event['Movie.Start_Offset_Seconds']
|
||||
# duration = event['PT.Clip.Finish_Seconds'] - event['PT.Clip.Start_Seconds']
|
||||
# duration = event['PT.Clip.Finish_Seconds'] -
|
||||
# event['PT.Clip.Start_Seconds']
|
||||
# input_movie = event['Movie.Filename']
|
||||
# print("Will make movie starting at {}, dur {} from movie {}".format(start, duration, input_movie))
|
||||
# print("Will make movie starting at {}, dur {} from movie {}"
|
||||
# .format(start, duration, input_movie))
|
||||
#
|
||||
#
|
||||
# def export_movies(events):
|
||||
@@ -1,22 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
from reportlab.pdfbase.pdfmetrics import (getAscent, getDescent)
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.pdfmetrics import getAscent, getDescent
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.platypus.doctemplate import BaseDocTemplate, PageTemplate
|
||||
from reportlab.platypus.frames import Frame
|
||||
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
|
||||
from typing import List
|
||||
|
||||
# TODO: A Generic report useful for spotting
|
||||
# TODO: A report useful for M&E mixer's notes
|
||||
# TODO: Use a default font that doesn't need to be installed
|
||||
|
||||
# This is from https://code.activestate.com/recipes/576832/ for
|
||||
# generating page count messages
|
||||
|
||||
|
||||
class ReportCanvas(canvas.Canvas):
|
||||
def __init__(self, *args, **kwargs):
|
||||
canvas.Canvas.__init__(self, *args, **kwargs)
|
||||
@@ -25,7 +26,7 @@ class ReportCanvas(canvas.Canvas):
|
||||
|
||||
def showPage(self):
|
||||
self._saved_page_states.append(dict(self.__dict__))
|
||||
self._startPage()
|
||||
self._startPage() # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
def save(self):
|
||||
"""add page info to each page (page x of y)"""
|
||||
@@ -38,10 +39,16 @@ class ReportCanvas(canvas.Canvas):
|
||||
|
||||
def draw_page_number(self, page_count):
|
||||
self.saveState()
|
||||
self.setFont('Helvetica', 10) #FIXME make this customizable
|
||||
self.drawString(0.5 * inch, 0.5 * inch, "Page %d of %d" % (self._pageNumber, page_count))
|
||||
right_edge = self._pagesize[0] - 0.5 * inch
|
||||
self.drawRightString(right_edge, 0.5 * inch, self._report_date.strftime("%m/%d/%Y %H:%M"))
|
||||
self.setFont("Helvetica", 10) # FIXME make this customizable
|
||||
self.drawString(
|
||||
0.5 * inch,
|
||||
0.5 * inch,
|
||||
f"Page {self._pageNumber} of {page_count}", # pyright: ignore[reportAttributeAccessIssue]
|
||||
)
|
||||
right_edge = self._pagesize[0] - 0.5 * inch # pyright: ignore[reportAttributeAccessIssue]
|
||||
self.drawRightString(
|
||||
right_edge, 0.5 * inch, self._report_date.strftime("%m/%d/%Y %H:%M")
|
||||
)
|
||||
|
||||
top_line = self.beginPath()
|
||||
top_line.moveTo(0.5 * inch, 0.75 * inch)
|
||||
@@ -56,47 +63,63 @@ class ADRDocTemplate(BaseDocTemplate):
|
||||
BaseDocTemplate.build(self, flowables, filename, canvasmaker)
|
||||
|
||||
|
||||
def make_doc_template(page_size, filename, document_title,
|
||||
def make_doc_template(
|
||||
page_size: tuple[float, float],
|
||||
filename,
|
||||
document_title,
|
||||
title: str,
|
||||
supervisor: str,
|
||||
document_header: str,
|
||||
client: str,
|
||||
document_subheader: str,
|
||||
left_margin=0.5 * inch,
|
||||
fonts: List[TTFont] = []) -> ADRDocTemplate:
|
||||
fonts: list[TTFont] | None = None,
|
||||
) -> ADRDocTemplate:
|
||||
|
||||
if fonts is None:
|
||||
fonts = []
|
||||
|
||||
right_margin = top_margin = bottom_margin = 0.5 * inch
|
||||
page_box = GRect(0., 0., page_size[0], page_size[1])
|
||||
_, page_box = page_box.split_x(left_margin, direction='l')
|
||||
_, page_box = page_box.split_x(right_margin, direction='r')
|
||||
_, page_box = page_box.split_y(bottom_margin, direction='u')
|
||||
_, page_box = page_box.split_y(top_margin, direction='d')
|
||||
page_box: GRect = GRect(0.0, 0.0, page_size[0], page_size[1])
|
||||
_, page_box = page_box.split_x(left_margin, direction="l") # pyright: ignore[reportAssignmentType]
|
||||
_, page_box = page_box.split_x(right_margin, direction="r") # pyright: ignore[reportAssignmentType]
|
||||
_, page_box = page_box.split_y(bottom_margin, direction="u") # pyright: ignore[reportAssignmentType]
|
||||
_, page_box = page_box.split_y(top_margin, direction="d") # pyright: ignore[reportAssignmentType]
|
||||
|
||||
footer_box, page_box = page_box.split_y(0.25 * inch, direction='u')
|
||||
header_box, page_box = page_box.split_y(0.75 * inch, direction='d')
|
||||
title_box, report_box = header_box.split_x(3.5 * inch, direction='r')
|
||||
footer_box, page_box = page_box.split_y(0.25 * inch, direction="u") # pyright: ignore[reportAssignmentType]
|
||||
header_box, page_box = page_box.split_y(0.75 * inch, direction="d") # pyright: ignore[reportAssignmentType]
|
||||
title_box, report_box = header_box.split_x(3.5 * inch, direction="r") # pyright: ignore[reportOptionalMemberAccess]
|
||||
|
||||
on_page_lambda = (lambda c, _:
|
||||
draw_header_footer(c, report_box, title_box,
|
||||
footer_box,title=title,
|
||||
def on_page_lambda(c, _):
|
||||
draw_header_footer(
|
||||
c,
|
||||
report_box,
|
||||
title_box,
|
||||
footer_box,
|
||||
title=title,
|
||||
supervisor=supervisor,
|
||||
document_subheader=document_subheader,
|
||||
client=client, doc_title=document_header))
|
||||
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=frames,
|
||||
onPage=on_page_lambda)
|
||||
page_template = PageTemplate(id="Main", frames=frames, onPage=on_page_lambda)
|
||||
|
||||
for font in fonts:
|
||||
pdfmetrics.registerFont(font)
|
||||
|
||||
doc = ADRDocTemplate(filename,
|
||||
doc = ADRDocTemplate(
|
||||
filename,
|
||||
title=document_title,
|
||||
author=supervisor,
|
||||
pagesize=page_size,
|
||||
leftMargin=left_margin, rightMargin=right_margin,
|
||||
topMargin=top_margin, bottomMargin=bottom_margin)
|
||||
leftMargin=left_margin,
|
||||
rightMargin=right_margin,
|
||||
topMargin=top_margin,
|
||||
bottomMargin=bottom_margin,
|
||||
)
|
||||
|
||||
doc.addPageTemplates([page_template])
|
||||
|
||||
@@ -109,22 +132,43 @@ def time_format(mins, zero_str="-"):
|
||||
"""
|
||||
if mins is None:
|
||||
return zero_str
|
||||
if mins == 0. and zero_str is not None:
|
||||
if mins == 0.0 and zero_str is not None:
|
||||
return zero_str
|
||||
elif mins < 60.:
|
||||
return "%im" % round(mins)
|
||||
elif mins < 60.0:
|
||||
return f"{round(mins)}m"
|
||||
else:
|
||||
m = round(mins)
|
||||
hh, mm = divmod(m, 60)
|
||||
return "%i:%02i" % (hh, mm)
|
||||
return f"{hh}:{mm:02}"
|
||||
|
||||
|
||||
def draw_header_footer(a_canvas: ReportCanvas, left_box, right_box, footer_box, title: str, supervisor: str,
|
||||
document_subheader: str, client: str, doc_title="", font_name='Helvetica'):
|
||||
def draw_header_footer(
|
||||
a_canvas: ReportCanvas,
|
||||
left_box,
|
||||
right_box,
|
||||
footer_box,
|
||||
title: str,
|
||||
supervisor: str,
|
||||
document_subheader: str,
|
||||
client: str,
|
||||
doc_title="",
|
||||
font_name="Helvetica",
|
||||
):
|
||||
|
||||
(_supervisor_box, client_box,), title_box = right_box.divide_y([16., 16., ])
|
||||
title_box.draw_text_cell(a_canvas, title, font_name, 18, inset_y=2., inset_x=5.)
|
||||
client_box.draw_text_cell(a_canvas, client, font_name, 11, inset_y=2., inset_x=5.)
|
||||
(
|
||||
(
|
||||
_supervisor_box,
|
||||
client_box,
|
||||
),
|
||||
title_box,
|
||||
) = right_box.divide_y(
|
||||
[
|
||||
16.0,
|
||||
16.0,
|
||||
]
|
||||
)
|
||||
title_box.draw_text_cell(a_canvas, title, font_name, 18, inset_y=2.0, inset_x=5.0)
|
||||
client_box.draw_text_cell(a_canvas, client, font_name, 11, inset_y=2.0, inset_x=5.0)
|
||||
|
||||
a_canvas.saveState()
|
||||
a_canvas.setLineWidth(0.5)
|
||||
@@ -139,16 +183,26 @@ def draw_header_footer(a_canvas: ReportCanvas, left_box, right_box, footer_box,
|
||||
a_canvas.drawPath(tline2)
|
||||
a_canvas.restoreState()
|
||||
|
||||
(doc_title_cell, spotting_version_cell,), _ = left_box.divide_y([18., 14], direction='d')
|
||||
(
|
||||
(
|
||||
doc_title_cell,
|
||||
spotting_version_cell,
|
||||
),
|
||||
_,
|
||||
) = left_box.divide_y([18.0, 14], direction="d")
|
||||
|
||||
doc_title_cell.draw_text_cell(a_canvas, doc_title, font_name, 14., inset_y=2.)
|
||||
doc_title_cell.draw_text_cell(a_canvas, doc_title, font_name, 14.0, inset_y=2.0)
|
||||
|
||||
if document_subheader is not None:
|
||||
spotting_version_cell.draw_text_cell(a_canvas, document_subheader, font_name, 12., inset_y=2.)
|
||||
spotting_version_cell.draw_text_cell(
|
||||
a_canvas, document_subheader, font_name, 12.0, inset_y=2.0
|
||||
)
|
||||
|
||||
if supervisor is not None:
|
||||
a_canvas.setFont(font_name, 11.)
|
||||
a_canvas.drawCentredString(footer_box.min_x + footer_box.width / 2., footer_box.min_y, supervisor)
|
||||
a_canvas.setFont(font_name, 11.0)
|
||||
a_canvas.drawCentredString(
|
||||
footer_box.min_x + footer_box.width / 2.0, footer_box.min_y, supervisor
|
||||
)
|
||||
|
||||
|
||||
class GRect:
|
||||
@@ -185,65 +239,75 @@ class GRect:
|
||||
return self.y + self.height / 2
|
||||
|
||||
def normalize(self):
|
||||
if self.width < 0.:
|
||||
if self.width < 0.0:
|
||||
self.width = abs(self.width)
|
||||
self.x = self.x - self.width
|
||||
|
||||
if self.height < 0.:
|
||||
if self.height < 0.0:
|
||||
self.height = abs(self.height)
|
||||
self.y = self.y - self.height
|
||||
|
||||
def split_x(self, at, direction='l'):
|
||||
def split_x(self, at, direction="l"):
|
||||
if at >= self.width:
|
||||
return None, self
|
||||
elif at <= 0:
|
||||
return self, None
|
||||
else:
|
||||
if direction == 'l':
|
||||
return (GRect(self.min_x, self.min_y, at, self.height),
|
||||
GRect(self.min_x + at, self.y, self.width - at, self.height))
|
||||
if direction == "l":
|
||||
return (
|
||||
GRect(self.min_x, self.min_y, at, self.height),
|
||||
GRect(self.min_x + at, self.y, self.width - at, self.height),
|
||||
)
|
||||
else:
|
||||
return (GRect(self.max_x - at, self.y, at, self.height),
|
||||
GRect(self.min_x, self.y, self.width - at, self.height))
|
||||
return (
|
||||
GRect(self.max_x - at, self.y, at, self.height),
|
||||
GRect(self.min_x, self.y, self.width - at, self.height),
|
||||
)
|
||||
|
||||
def split_y(self, at, direction='u'):
|
||||
def split_y(self, at, direction="u"):
|
||||
if at >= self.height:
|
||||
return None, self
|
||||
elif at <= 0:
|
||||
return self, None
|
||||
else:
|
||||
if direction == 'u':
|
||||
return (GRect(self.x, self.y, self.width, at),
|
||||
GRect(self.x, self.y + at, self.width, self.height - at))
|
||||
if direction == "u":
|
||||
return (
|
||||
GRect(self.x, self.y, self.width, at),
|
||||
GRect(self.x, self.y + at, self.width, self.height - at),
|
||||
)
|
||||
else:
|
||||
return (GRect(self.x, self.max_y - at, self.width, at),
|
||||
GRect(self.x, self.y, self.width, self.height - at))
|
||||
return (
|
||||
GRect(self.x, self.max_y - at, self.width, at),
|
||||
GRect(self.x, self.y, self.width, self.height - at),
|
||||
)
|
||||
|
||||
def inset_xy(self, dx, dy):
|
||||
return GRect(self.x + dx, self.y + dy, self.width - dx * 2, self.height - dy * 2)
|
||||
return GRect(
|
||||
self.x + dx, self.y + dy, self.width - dx * 2, self.height - dy * 2
|
||||
)
|
||||
|
||||
def inset(self, d):
|
||||
return self.inset_xy(d, d)
|
||||
|
||||
def __repr__(self):
|
||||
return "<GRect x=%f y=%f width=%f height=%f>" % (self.x, self.y, self.width, self.height)
|
||||
return f"<GRect x={self.x} y={self.y} width={self.width} height={self.height}>"
|
||||
|
||||
def divide_x(self, x_list, direction='l'):
|
||||
ret_list = list()
|
||||
def divide_x(self, x_list, direction="l"):
|
||||
ret_list = []
|
||||
|
||||
rem = self
|
||||
for item in x_list:
|
||||
s, rem = rem.split_x(item, direction)
|
||||
s, rem = rem.split_x(item, direction) # pyright: ignore[reportOptionalMemberAccess]
|
||||
ret_list.append(s)
|
||||
|
||||
return ret_list, rem
|
||||
|
||||
def divide_y(self, y_list, direction='u'):
|
||||
ret_list = list()
|
||||
def divide_y(self, y_list, direction="u"):
|
||||
ret_list = []
|
||||
|
||||
rem = self
|
||||
for item in y_list:
|
||||
s, rem = rem.split_y(item, direction)
|
||||
s, rem = rem.split_y(item, direction) # pyright: ignore[reportOptionalMemberAccess]
|
||||
ret_list.append(s)
|
||||
|
||||
return ret_list, rem
|
||||
@@ -258,13 +322,13 @@ class GRect:
|
||||
def draw_border(self, a_canvas, edge):
|
||||
|
||||
def draw_border_impl(en):
|
||||
if en == 'min_x':
|
||||
if en == "min_x":
|
||||
coordinates = ((self.min_x, self.min_y), (self.min_x, self.max_y))
|
||||
elif en == 'max_x':
|
||||
elif en == "max_x":
|
||||
coordinates = ((self.max_x, self.min_y), (self.max_x, self.max_y))
|
||||
elif en == 'min_y':
|
||||
elif en == "min_y":
|
||||
coordinates = ((self.min_x, self.min_y), (self.max_x, self.min_y))
|
||||
elif en == 'max_y':
|
||||
elif en == "max_y":
|
||||
coordinates = ((self.min_x, self.max_y), (self.max_x, self.max_y))
|
||||
else:
|
||||
return
|
||||
@@ -280,9 +344,18 @@ class GRect:
|
||||
for e in edge:
|
||||
draw_border_impl(e)
|
||||
|
||||
def draw_text_cell(self, a_canvas, text, font_name, font_size,
|
||||
vertical_align='t', force_baseline=None, inset_x=0.,
|
||||
inset_y=0., draw_baseline=False):
|
||||
def draw_text_cell(
|
||||
self,
|
||||
a_canvas,
|
||||
text,
|
||||
font_name,
|
||||
font_size,
|
||||
vertical_align="t",
|
||||
force_baseline=None,
|
||||
inset_x=0.0,
|
||||
inset_y=0.0,
|
||||
draw_baseline=False,
|
||||
):
|
||||
if text is None:
|
||||
return
|
||||
|
||||
@@ -290,9 +363,9 @@ class GRect:
|
||||
|
||||
inset_rect = self.inset_xy(inset_x, inset_y)
|
||||
|
||||
if vertical_align == 'm':
|
||||
y = inset_rect.center_y - getAscent(font_name, font_size) / 2.
|
||||
elif vertical_align == 't':
|
||||
if vertical_align == "m":
|
||||
y = inset_rect.center_y - getAscent(font_name, font_size) / 2.0
|
||||
elif vertical_align == "t":
|
||||
y = inset_rect.max_y - getAscent(font_name, font_size)
|
||||
else:
|
||||
y = inset_rect.min_y - getDescent(font_name, font_size)
|
||||
@@ -314,14 +387,15 @@ class GRect:
|
||||
a_canvas.setDash([3.0, 1.0, 2.0, 1.0])
|
||||
a_canvas.setLineWidth(0.5)
|
||||
bl = a_canvas.beginPath()
|
||||
bl.moveTo(inset_rect.min_x, y - 1.)
|
||||
bl.lineTo(inset_rect.max_x, y - 1.)
|
||||
bl.moveTo(inset_rect.min_x, y - 1.0)
|
||||
bl.lineTo(inset_rect.max_x, y - 1.0)
|
||||
a_canvas.drawPath(bl)
|
||||
|
||||
a_canvas.restoreState()
|
||||
|
||||
def draw_flowable(self, a_canvas, flowable, inset_x=0.,
|
||||
inset_y=0., draw_baselines=False):
|
||||
def draw_flowable(
|
||||
self, a_canvas, flowable, inset_x=0.0, inset_y=0.0, draw_baselines=False
|
||||
):
|
||||
a_canvas.saveState()
|
||||
|
||||
inset_rect = self.inset_xy(inset_x, inset_y)
|
||||
@@ -330,7 +404,7 @@ class GRect:
|
||||
cp.rect(self.min_x, self.min_y, self.width, self.height)
|
||||
a_canvas.clipPath(cp, stroke=0, fill=0)
|
||||
|
||||
w, h = flowable.wrap(inset_rect.width, inset_rect.height)
|
||||
_w, h = flowable.wrap(inset_rect.width, inset_rect.height)
|
||||
|
||||
flowable.drawOn(a_canvas, inset_rect.x, inset_rect.max_y - h)
|
||||
|
||||
@@ -339,7 +413,7 @@ class GRect:
|
||||
a_canvas.setLineWidth(0.5)
|
||||
leading = flowable.style.leading
|
||||
|
||||
y = inset_rect.max_y - flowable.style.fontSize - 1.
|
||||
y = inset_rect.max_y - flowable.style.fontSize - 1.0
|
||||
while y > inset_rect.min_x:
|
||||
bl = a_canvas.beginPath()
|
||||
bl.moveTo(inset_rect.min_x, y)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,366 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import letter, portrait
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
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: list[str] | None,
|
||||
show_priorities=False,
|
||||
include_omitted=False,
|
||||
):
|
||||
columns = []
|
||||
reel_numbers = reel_list or sorted({x.reel for x in lines if x.reel is not None})
|
||||
|
||||
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": f"P{n}",
|
||||
"value_getter": lambda recs, N=n: len(
|
||||
[r for r in recs if r.priority == N]
|
||||
),
|
||||
"value_getter2": (
|
||||
lambda recs, N=n: 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 = []
|
||||
styles = []
|
||||
columns_widths = []
|
||||
|
||||
sorted_character_numbers: list[str | None] = sorted(
|
||||
{x.character_id for x in lines}, key=lambda x: str(x)
|
||||
)
|
||||
|
||||
for i, c in enumerate(columns):
|
||||
styles.extend(c["style_getter"](i))
|
||||
columns_widths.append(c["width"])
|
||||
|
||||
data.append([x["heading"] for x in columns])
|
||||
|
||||
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 = []
|
||||
row_data2 = []
|
||||
|
||||
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 = []
|
||||
summary_row2 = []
|
||||
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],
|
||||
page_size: tuple[float, float] | None = None,
|
||||
include_omitted=False,
|
||||
font_name="Helvetica",
|
||||
):
|
||||
if page_size is None:
|
||||
page_size = portrait(letter)
|
||||
|
||||
columns = build_columns(lines, include_omitted=include_omitted, reel_list=reel_list)
|
||||
data, style, columns_widths = populate_columns(
|
||||
lines, columns, include_omitted, page_size
|
||||
)
|
||||
|
||||
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 = f"{lines[0].title} Line Count"
|
||||
filename = title + ".pdf"
|
||||
|
||||
doc = make_doc_template(
|
||||
page_size=page_size,
|
||||
filename=filename,
|
||||
document_title=title,
|
||||
title=lines[0].title,
|
||||
document_subheader=lines[0].spot or "",
|
||||
client=lines[0].client or "",
|
||||
supervisor=lines[0].supervisor or "",
|
||||
document_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(f"* {omitted_count} Omitted lines are excluded.", style))
|
||||
|
||||
doc.build(story)
|
||||
@@ -1,5 +1,6 @@
|
||||
# TODO: Complete Recordist Log
|
||||
|
||||
|
||||
def output_report(records):
|
||||
# order by start
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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 = []
|
||||
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 += (
|
||||
f"<font backColor={bg_color} textColor={fg_color} fontSize=11>EFF</font> "
|
||||
)
|
||||
elif line.tv:
|
||||
bg_color = "blue"
|
||||
tag_field += (
|
||||
f"<font backColor={bg_color} textColor={fg_color} fontSize=11>TV</font> "
|
||||
)
|
||||
elif line.adlib:
|
||||
bg_color = "purple"
|
||||
tag_field += (
|
||||
f"<font backColor={bg_color} textColor={fg_color} fontSize=11>ADLIB</font> "
|
||||
)
|
||||
elif line.optional:
|
||||
bg_color = "green"
|
||||
tag_field += f"font backColor={bg_color} textColor={fg_color} fontSize=11>OPTIONAL</font>"
|
||||
|
||||
entries.append(tag_field)
|
||||
|
||||
return "<br />".join(entries)
|
||||
|
||||
|
||||
def build_story(lines: list[ADRLine], tc_rate: TimecodeFormat, font_name="Helvetica"):
|
||||
story = []
|
||||
|
||||
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 = (
|
||||
f"{line.cue_number}<br /><font fontSize=7>{line.character_name}</font>"
|
||||
)
|
||||
|
||||
time_data = time_format(line.time_budget_mins)
|
||||
|
||||
if line.priority is not None:
|
||||
time_data = time_data + "<br />" + "P: " + str(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 or "[No 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(f"{line.reel}")
|
||||
else:
|
||||
third_line.append(f"Reel {line.reel}")
|
||||
if line.version is not None:
|
||||
third_line.append(f"({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 = f"{lines[0].title} ADR Report ({lines[0].character_name})"
|
||||
document_header = f"{lines[0].character_name} ADR Report"
|
||||
else:
|
||||
title = f"{lines[0].title} ADR Report"
|
||||
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 or "",
|
||||
client=lines[0].client or "",
|
||||
document_subheader=lines[0].spot or "",
|
||||
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: tuple[float, float] | None = None,
|
||||
by_character: bool = False,
|
||||
):
|
||||
if page_size is None:
|
||||
page_size = portrait(letter)
|
||||
|
||||
if by_character:
|
||||
character_numbers = {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)
|
||||
@@ -0,0 +1,345 @@
|
||||
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 = f"{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, value in tags.items():
|
||||
if getattr(record, key):
|
||||
tag_field = tag_field + value + " "
|
||||
|
||||
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, _ = 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 = [f"Line {line_no} of {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 = f"{records[0].title}_{records[0].character_id}_{records[0].character_name}_Log.pdf"
|
||||
|
||||
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(
|
||||
f"{records[0].title} {records[0].character_name} ({records[0].character_id}) Supervisor's Log"
|
||||
)
|
||||
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 = {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,
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
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 = {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 = f"{char_lines[0].title} ({character_name}) {n} ADR Script"
|
||||
filename = f"{char_lines[0].title}_{n}_{character_name}_ADR Script.pdf"
|
||||
|
||||
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 or "[No QN]", 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 or "[No Prompt]", prompt_style),
|
||||
Spacer(1.0, inch * 1.5),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
doc.build(story)
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
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")
|
||||
@@ -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)
|
||||
@@ -0,0 +1,214 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from importlib.metadata import version as module_version
|
||||
from xml.etree.ElementTree import TreeBuilder, tostring
|
||||
|
||||
import ptulsconv
|
||||
from ptulsconv.docparser.adr_entity import ADRLine
|
||||
|
||||
# TODO Get a third-party test for Avid Marker lists
|
||||
|
||||
|
||||
def avid_marker_list(
|
||||
lines: list[ADRLine],
|
||||
report_date: datetime | None = None,
|
||||
reel_start_frame=0,
|
||||
fps=24,
|
||||
):
|
||||
doc = TreeBuilder(element_factory=None)
|
||||
|
||||
if report_date is None:
|
||||
report_date = datetime.now()
|
||||
|
||||
doc.start("Avid:StreamItems", {"xmlns:Avid": "http://www.avid.com"})
|
||||
doc.start("Avid:XMLFileData", {})
|
||||
doc.start("AvProp", {"name": "DomainMagic", "type": "string"})
|
||||
doc.data("Domain")
|
||||
doc.end("AvProp")
|
||||
doc.start("AvProp", {"name": "DomainKey", "type": "string"})
|
||||
doc.data("58424a44")
|
||||
doc.end("AvProp")
|
||||
|
||||
def insert_elem(kind, attb, atype, name, value):
|
||||
doc.start("ListElem", {})
|
||||
doc.start("AvProp", {"id": "ATTR", "name": "OMFI:ATTB:Kind", "type": "int32"})
|
||||
doc.data(kind)
|
||||
doc.end("AvProp")
|
||||
|
||||
doc.start("AvProp", {"id": "ATTR", "name": "OMFI:ATTB:Name", "type": "string"})
|
||||
doc.data(name)
|
||||
doc.end("AvProp")
|
||||
|
||||
doc.start("AvProp", {"id": "ATTR", "name": attb, "type": atype})
|
||||
doc.data(value)
|
||||
doc.end("AvProp")
|
||||
|
||||
doc.end("ListElem")
|
||||
|
||||
for line in lines:
|
||||
doc.start("AvClass", {"id": "ATTR"})
|
||||
doc.start(
|
||||
"AvProp", {"id": "ATTR", "name": "__OMFI:ATTR:NumItems", "type": "int32"}
|
||||
)
|
||||
doc.data("7")
|
||||
doc.end("AvProp")
|
||||
|
||||
doc.start("List", {"id": "OMFI:ATTR:AttrRefs"})
|
||||
|
||||
assert report_date
|
||||
|
||||
insert_elem(
|
||||
"1",
|
||||
"OMFI:ATTB:IntAttribute",
|
||||
"int32",
|
||||
"_ATN_CRM_LONG_CREATE_DATE",
|
||||
report_date.strftime("%s"),
|
||||
)
|
||||
insert_elem(
|
||||
"2", "OMFI:ATTB:StringAttribute", "string", "_ATN_CRM_COLOR", "yellow"
|
||||
)
|
||||
insert_elem(
|
||||
"2",
|
||||
"OMFI:ATTB:StringAttribute",
|
||||
"string",
|
||||
"_ATN_CRM_USER",
|
||||
line.supervisor or "",
|
||||
)
|
||||
|
||||
marker_name = f"{line.cue_number}: {line.prompt}"
|
||||
insert_elem(
|
||||
"2", "OMFI:ATTB:StringAttribute", "string", "_ATN_CRM_COM", marker_name
|
||||
)
|
||||
|
||||
start_frame = int(line.start * fps)
|
||||
|
||||
insert_elem(
|
||||
"2",
|
||||
"OMFI:ATTB:StringAttribute",
|
||||
"string",
|
||||
"_ATN_CRM_TC",
|
||||
str(start_frame - reel_start_frame),
|
||||
)
|
||||
|
||||
insert_elem("2", "OMFI:ATTB:StringAttribute", "string", "_ATN_CRM_TRK", "V1")
|
||||
insert_elem("1", "OMFI:ATTB:IntAttribute", "int32", "_ATN_CRM_LENGTH", "1")
|
||||
|
||||
doc.start("ListElem", {})
|
||||
doc.end("ListElem")
|
||||
|
||||
doc.end("List")
|
||||
doc.end("AvClass")
|
||||
|
||||
doc.end("Avid:XMLFileData")
|
||||
doc.end("Avid:StreamItems")
|
||||
|
||||
|
||||
def dump_fmpxml(data, input_file_name, output, adr_field_map):
|
||||
doc = TreeBuilder(element_factory=None)
|
||||
|
||||
doc.start("FMPXMLRESULT", {"xmlns": "http://www.filemaker.com/fmpxmlresult"})
|
||||
|
||||
doc.start("ERRORCODE", {})
|
||||
doc.data("0")
|
||||
doc.end("ERRORCODE")
|
||||
|
||||
version = module_version("ptulsconv")
|
||||
doc.start("PRODUCT", {"NAME": ptulsconv.__name__, "VERSION": f"{version}"})
|
||||
doc.end("PRODUCT")
|
||||
|
||||
doc.start(
|
||||
"DATABASE",
|
||||
{
|
||||
"DATEFORMAT": "MM/dd/yy",
|
||||
"LAYOUT": "summary",
|
||||
"TIMEFORMAT": "hh:mm:ss",
|
||||
"RECORDS": str(len(data["events"])),
|
||||
"NAME": os.path.basename(input_file_name),
|
||||
},
|
||||
)
|
||||
doc.end("DATABASE")
|
||||
|
||||
doc.start("METADATA", {})
|
||||
for field in adr_field_map:
|
||||
tp = field[2]
|
||||
ft = "TEXT"
|
||||
if tp is int or tp is float:
|
||||
ft = "NUMBER"
|
||||
|
||||
doc.start(
|
||||
"FIELD", {"EMPTYOK": "YES", "MAXREPEAT": "1", "NAME": field[1], "TYPE": ft}
|
||||
)
|
||||
doc.end("FIELD")
|
||||
doc.end("METADATA")
|
||||
|
||||
doc.start("RESULTSET", {"FOUND": str(len(data["events"]))})
|
||||
for event in data["events"]:
|
||||
doc.start("ROW", {})
|
||||
for field in adr_field_map:
|
||||
doc.start("COL", {})
|
||||
doc.start("DATA", {})
|
||||
for key_attempt in field[0]:
|
||||
if key_attempt in event:
|
||||
doc.data(str(event[key_attempt]))
|
||||
break
|
||||
doc.end("DATA")
|
||||
doc.end("COL")
|
||||
doc.end("ROW")
|
||||
doc.end("RESULTSET")
|
||||
|
||||
doc.end("FMPXMLRESULT")
|
||||
docelem = doc.close()
|
||||
xmlstr = tostring(docelem, encoding="unicode", method="xml")
|
||||
output.write(xmlstr)
|
||||
|
||||
|
||||
xslt_path = os.path.join(pathlib.Path(__file__).parent.absolute(), "xslt")
|
||||
|
||||
|
||||
def xform_options():
|
||||
return glob.glob(os.path.join(xslt_path, "*.xsl"))
|
||||
|
||||
|
||||
def dump_xform_options(output=sys.stdout):
|
||||
print("# Available transforms:", file=output)
|
||||
print(f"# Transform dir: {xslt_path}", file=output)
|
||||
for f in xform_options():
|
||||
base = os.path.basename(f)
|
||||
name, _ = os.path.splitext(base)
|
||||
print("# " + name, file=output)
|
||||
|
||||
|
||||
def fmp_transformed_dump(data, input_file, xsl_name, output, adr_field_map):
|
||||
import io
|
||||
|
||||
from ptulsconv.reporting import print_status_style
|
||||
|
||||
pipe = io.StringIO()
|
||||
|
||||
print_status_style("Generating base XML")
|
||||
dump_fmpxml(data, input_file, pipe, adr_field_map)
|
||||
|
||||
str_data = pipe.getvalue()
|
||||
print_status_style(f"Base XML size {len(str_data)}")
|
||||
|
||||
print_status_style("Running xsltproc")
|
||||
|
||||
xsl_path = os.path.join(
|
||||
pathlib.Path(__file__).parent.absolute(), "xslt", xsl_name + ".xsl"
|
||||
)
|
||||
print_status_style(f"Using xsl: {xsl_path}")
|
||||
subprocess.run(
|
||||
["xsltproc", xsl_path, "-"],
|
||||
input=str_data,
|
||||
text=True,
|
||||
stdout=output,
|
||||
shell=False,
|
||||
check=True,
|
||||
)
|
||||
@@ -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
|
||||
@@ -1,25 +1,27 @@
|
||||
import unittest
|
||||
from ptulsconv.docparser import parse_document
|
||||
import os.path
|
||||
import unittest
|
||||
|
||||
from ptulsconv.docparser import parse_document
|
||||
|
||||
|
||||
class TestRobinHood1(unittest.TestCase):
|
||||
path = os.path.dirname(__file__) + '/../export_cases/Robin Hood Spotting.txt'
|
||||
path = os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting.txt"
|
||||
|
||||
def test_header_export(self):
|
||||
|
||||
session = parse_document(self.path)
|
||||
with open(self.path, "r") as file:
|
||||
session = parse_document(file.read())
|
||||
|
||||
self.assertIsNotNone(session.header)
|
||||
self.assertEqual(session.header.session_name, 'Robin Hood Spotting')
|
||||
self.assertEqual(session.header.session_name, "Robin Hood Spotting")
|
||||
self.assertEqual(session.header.sample_rate, 48000.0)
|
||||
self.assertEqual(session.header.bit_depth, 24)
|
||||
self.assertEqual(session.header.timecode_fps, '29.97')
|
||||
self.assertEqual(session.header.timecode_fps, "29.97")
|
||||
self.assertEqual(session.header.timecode_drop_frame, False)
|
||||
|
||||
def test_all_sections(self):
|
||||
|
||||
session = parse_document(self.path)
|
||||
with open(self.path, "r") as file:
|
||||
session = parse_document(file.read())
|
||||
|
||||
self.assertIsNotNone(session.header)
|
||||
self.assertIsNotNone(session.files)
|
||||
@@ -30,15 +32,33 @@ class TestRobinHood1(unittest.TestCase):
|
||||
|
||||
def test_tracks(self):
|
||||
|
||||
session = parse_document(self.path)
|
||||
with open(self.path, "r") as file:
|
||||
session = parse_document(file.read())
|
||||
|
||||
self.assertEqual(len(session.tracks), 14)
|
||||
self.assertListEqual(["Scenes", "Robin", "Will", "Marian", "John",
|
||||
"Guy", "Much", "Butcher", "Town Crier",
|
||||
"Soldier 1", "Soldier 2", "Soldier 3",
|
||||
"Priest", "Guest at Court"],
|
||||
list(map(lambda t: t.name, session.tracks)))
|
||||
self.assertListEqual(["", "[ADR] {Actor=Errol Flynn} $CN=1",
|
||||
self.assertListEqual(
|
||||
[
|
||||
"Scenes",
|
||||
"Robin",
|
||||
"Will",
|
||||
"Marian",
|
||||
"John",
|
||||
"Guy",
|
||||
"Much",
|
||||
"Butcher",
|
||||
"Town Crier",
|
||||
"Soldier 1",
|
||||
"Soldier 2",
|
||||
"Soldier 3",
|
||||
"Priest",
|
||||
"Guest at Court",
|
||||
],
|
||||
list(map(lambda t: t.name, session.tracks)),
|
||||
)
|
||||
self.assertListEqual(
|
||||
[
|
||||
"",
|
||||
"[ADR] {Actor=Errol Flynn} $CN=1",
|
||||
"[ADR] {Actor=Patrick Knowles} $CN=2",
|
||||
"[ADR] {Actor=Olivia DeHavilland} $CN=3",
|
||||
"[ADR] {Actor=Claude Raines} $CN=4",
|
||||
@@ -50,28 +70,37 @@ class TestRobinHood1(unittest.TestCase):
|
||||
"[ADR] $CN=104",
|
||||
"[ADR] $CN=105",
|
||||
"[ADR] {Actor=Thomas R. Mills} $CN=106",
|
||||
"[ADR] $CN=107"],
|
||||
list(map(lambda t: t.comments, session.tracks)))
|
||||
"[ADR] $CN=107",
|
||||
],
|
||||
list(map(lambda t: t.comments, session.tracks)),
|
||||
)
|
||||
|
||||
def test_a_track(self):
|
||||
session = parse_document(self.path)
|
||||
|
||||
with open(self.path, "r") as file:
|
||||
session = parse_document(file.read())
|
||||
|
||||
guy_track = session.tracks[5]
|
||||
self.assertEqual(guy_track.name, 'Guy')
|
||||
self.assertEqual(guy_track.comments, '[ADR] {Actor=Basil Rathbone} $CN=5')
|
||||
self.assertEqual(guy_track.name, "Guy")
|
||||
self.assertEqual(guy_track.comments, "[ADR] {Actor=Basil Rathbone} $CN=5")
|
||||
self.assertEqual(guy_track.user_delay_samples, 0)
|
||||
self.assertListEqual(guy_track.state, [])
|
||||
self.assertEqual(len(guy_track.clips), 16)
|
||||
self.assertEqual(guy_track.clips[5].channel, 1)
|
||||
self.assertEqual(guy_track.clips[5].event, 6)
|
||||
self.assertEqual(guy_track.clips[5].clip_name, "\"What's your name? You Saxon dog!\" $QN=GY106")
|
||||
self.assertEqual(
|
||||
guy_track.clips[5].clip_name,
|
||||
'"What\'s your name? You Saxon dog!" $QN=GY106',
|
||||
)
|
||||
self.assertEqual(guy_track.clips[5].start_timecode, "01:04:19:15")
|
||||
self.assertEqual(guy_track.clips[5].finish_timecode, "01:04:21:28")
|
||||
self.assertEqual(guy_track.clips[5].duration, "00:00:02:13")
|
||||
self.assertEqual(guy_track.clips[5].timestamp, None)
|
||||
self.assertEqual(guy_track.clips[5].state, 'Unmuted')
|
||||
self.assertEqual(guy_track.clips[5].state, "Unmuted")
|
||||
|
||||
def test_memory_locations(self):
|
||||
session = parse_document(self.path)
|
||||
with open(self.path, "r") as file:
|
||||
session = parse_document(file.read())
|
||||
|
||||
self.assertEqual(len(session.markers), 1)
|
||||
self.assertEqual(session.markers[0].number, 1)
|
||||
@@ -80,5 +109,5 @@ class TestRobinHood1(unittest.TestCase):
|
||||
self.assertEqual(session.markers[0].units, "Samples")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,45 +1,56 @@
|
||||
import unittest
|
||||
from ptulsconv.docparser import parse_document
|
||||
import os.path
|
||||
import unittest
|
||||
|
||||
from ptulsconv.docparser import parse_document
|
||||
|
||||
|
||||
class TestRobinHood5(unittest.TestCase):
|
||||
path = os.path.dirname(__file__) + '/../export_cases/Robin Hood Spotting5.txt'
|
||||
path = os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting5.txt"
|
||||
|
||||
def test_skipped_segments(self):
|
||||
session = parse_document(self.path)
|
||||
with open(self.path, "r") as file:
|
||||
session = parse_document(file.read())
|
||||
|
||||
self.assertIsNone(session.files)
|
||||
self.assertIsNone(session.clips)
|
||||
|
||||
def test_plugins(self):
|
||||
session = parse_document(self.path)
|
||||
with open(self.path, "r") as file:
|
||||
session = parse_document(file.read())
|
||||
|
||||
self.assertEqual(len(session.plugins), 2)
|
||||
|
||||
def test_stereo_track(self):
|
||||
session = parse_document(self.path)
|
||||
self.assertEqual(session.tracks[1].name, 'MX WT (Stereo)')
|
||||
with open(self.path, "r") as file:
|
||||
session = parse_document(file.read())
|
||||
|
||||
self.assertEqual(session.tracks[1].name, "MX WT (Stereo)")
|
||||
self.assertEqual(len(session.tracks[1].clips), 2)
|
||||
self.assertEqual(session.tracks[1].clips[0].clip_name, 'RobinHood.1-01.L')
|
||||
self.assertEqual(session.tracks[1].clips[1].clip_name, 'RobinHood.1-01.R')
|
||||
self.assertEqual(session.tracks[1].clips[0].clip_name, "RobinHood.1-01.L")
|
||||
self.assertEqual(session.tracks[1].clips[1].clip_name, "RobinHood.1-01.R")
|
||||
|
||||
def test_a_track(self):
|
||||
session = parse_document(self.path)
|
||||
with open(self.path, "r") as file:
|
||||
session = parse_document(file.read())
|
||||
|
||||
guy_track = session.tracks[8]
|
||||
self.assertEqual(guy_track.name, 'Guy')
|
||||
self.assertEqual(guy_track.comments, '[ADR] {Actor=Basil Rathbone} $CN=5')
|
||||
self.assertEqual(guy_track.name, "Guy")
|
||||
self.assertEqual(guy_track.comments, "[ADR] {Actor=Basil Rathbone} $CN=5")
|
||||
self.assertEqual(guy_track.user_delay_samples, 0)
|
||||
self.assertListEqual(guy_track.state, ['Solo'])
|
||||
self.assertListEqual(guy_track.state, ["Solo"])
|
||||
self.assertEqual(len(guy_track.clips), 16)
|
||||
self.assertEqual(guy_track.clips[5].channel, 1)
|
||||
self.assertEqual(guy_track.clips[5].event, 6)
|
||||
self.assertEqual(guy_track.clips[5].clip_name, "\"What's your name? You Saxon dog!\" $QN=GY106")
|
||||
self.assertEqual(
|
||||
guy_track.clips[5].clip_name,
|
||||
'"What\'s your name? You Saxon dog!" $QN=GY106',
|
||||
)
|
||||
self.assertEqual(guy_track.clips[5].start_timecode, "01:04:19:15.00")
|
||||
self.assertEqual(guy_track.clips[5].finish_timecode, "01:04:21:28.00")
|
||||
self.assertEqual(guy_track.clips[5].duration, "00:00:02:13.00")
|
||||
self.assertEqual(guy_track.clips[5].timestamp, "01:04:19:09.70")
|
||||
self.assertEqual(guy_track.clips[5].state, 'Unmuted')
|
||||
self.assertEqual(guy_track.clips[5].state, "Unmuted")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
import unittest
|
||||
from ptulsconv.docparser import parse_document
|
||||
import os.path
|
||||
import unittest
|
||||
|
||||
from ptulsconv.docparser import parse_document
|
||||
|
||||
|
||||
class TestRobinHood6(unittest.TestCase):
|
||||
path = os.path.dirname(__file__) + '/../export_cases/Robin Hood Spotting6.txt'
|
||||
path = os.path.dirname(__file__) + "/../export_cases/Robin Hood Spotting6.txt"
|
||||
|
||||
def test_a_track(self):
|
||||
session = parse_document(self.path)
|
||||
with open(self.path, "r") as file:
|
||||
session = parse_document(file.read())
|
||||
|
||||
marian_track = session.tracks[6]
|
||||
self.assertEqual(marian_track.name, 'Marian')
|
||||
self.assertEqual(marian_track.comments, '[ADR] {Actor=Olivia DeHavilland} $CN=3')
|
||||
self.assertEqual(marian_track.name, "Marian")
|
||||
self.assertEqual(
|
||||
marian_track.comments, "[ADR] {Actor=Olivia DeHavilland} $CN=3"
|
||||
)
|
||||
self.assertEqual(marian_track.user_delay_samples, 0)
|
||||
self.assertListEqual(marian_track.state, ['Solo'])
|
||||
self.assertListEqual(marian_track.state, ["Solo"])
|
||||
self.assertEqual(len(marian_track.clips), 4)
|
||||
self.assertListEqual(marian_track.plugins, ['Channel Strip (mono)', 'ReVibe II (mono/5.1)'])
|
||||
self.assertListEqual(
|
||||
marian_track.plugins, ["Channel Strip (mono)", "ReVibe II (mono/5.1)"]
|
||||
)
|
||||
self.assertEqual(marian_track.clips[2].channel, 1)
|
||||
self.assertEqual(marian_track.clips[2].event, 3)
|
||||
self.assertEqual(marian_track.clips[2].clip_name,
|
||||
"\"Isn't that reason enough for a Royal Ward who must obey her guardian?\" $QN=M103")
|
||||
self.assertEqual(
|
||||
marian_track.clips[2].clip_name,
|
||||
'"Isn\'t that reason enough for a Royal Ward who must obey her guardian?" $QN=M103',
|
||||
)
|
||||
self.assertEqual(marian_track.clips[2].start_timecode, "01:08:01:11")
|
||||
self.assertEqual(marian_track.clips[2].finish_timecode, "01:08:04:24")
|
||||
self.assertEqual(marian_track.clips[2].duration, "00:00:03:12")
|
||||
self.assertEqual(marian_track.clips[2].timestamp, "01:08:01:11")
|
||||
self.assertEqual(marian_track.clips[2].state, 'Unmuted')
|
||||
self.assertEqual(marian_track.clips[2].state, "Unmuted")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
import unittest
|
||||
from ptulsconv.docparser import parse_document
|
||||
import os.path
|
||||
import unittest
|
||||
|
||||
from ptulsconv.docparser import parse_document
|
||||
|
||||
|
||||
class TestRobinHoodDF(unittest.TestCase):
|
||||
path = os.path.dirname(__file__) + '/../export_cases/Robin Hood SpottingDF.txt'
|
||||
path = os.path.dirname(__file__) + "/../export_cases/Robin Hood SpottingDF.txt"
|
||||
|
||||
def test_header_export_df(self):
|
||||
session = parse_document(self.path)
|
||||
|
||||
with open(self.path, "r") as file:
|
||||
session = parse_document(file.read())
|
||||
|
||||
self.assertEqual(session.header.timecode_drop_frame, True)
|
||||
|
||||
def test_a_track(self):
|
||||
session = parse_document(self.path)
|
||||
|
||||
with open(self.path, "r") as file:
|
||||
session = parse_document(file.read())
|
||||
|
||||
guy_track = session.tracks[4]
|
||||
self.assertEqual(guy_track.name, 'Robin')
|
||||
self.assertEqual(guy_track.comments, '[ADR] {Actor=Errol Flynn} $CN=1')
|
||||
self.assertEqual(guy_track.name, "Robin")
|
||||
self.assertEqual(guy_track.comments, "[ADR] {Actor=Errol Flynn} $CN=1")
|
||||
self.assertEqual(guy_track.user_delay_samples, 0)
|
||||
self.assertListEqual(guy_track.state, [])
|
||||
self.assertEqual(len(guy_track.clips), 10)
|
||||
self.assertEqual(guy_track.clips[5].channel, 1)
|
||||
self.assertEqual(guy_track.clips[5].event, 6)
|
||||
self.assertEqual(guy_track.clips[5].clip_name, "\"Hold there! What's his fault?\" $QN=R106")
|
||||
self.assertEqual(
|
||||
guy_track.clips[5].clip_name, '"Hold there! What\'s his fault?" $QN=R106'
|
||||
)
|
||||
self.assertEqual(guy_track.clips[5].start_timecode, "01:05:30;15")
|
||||
self.assertEqual(guy_track.clips[5].finish_timecode, "01:05:32;01")
|
||||
self.assertEqual(guy_track.clips[5].duration, "00:00:01;16")
|
||||
self.assertEqual(guy_track.clips[5].timestamp, None)
|
||||
self.assertEqual(guy_track.clips[5].state, 'Unmuted')
|
||||
self.assertEqual(guy_track.clips[5].state, "Unmuted")
|
||||
|
||||
@@ -1,34 +1,50 @@
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import tempfile
|
||||
|
||||
import os.path
|
||||
import os
|
||||
import glob
|
||||
|
||||
from ptulsconv import commands
|
||||
|
||||
|
||||
class TestPDFExport(unittest.TestCase):
|
||||
def test_report_generation(self):
|
||||
"""
|
||||
Setp through every text file in export_cases and make sure it can
|
||||
be converted into PDF docs without throwing an error
|
||||
"""
|
||||
files = []
|
||||
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:
|
||||
tempdir = tempfile.TemporaryDirectory()
|
||||
os.chdir(tempdir.name)
|
||||
try:
|
||||
commands.convert(path, major_mode='doc')
|
||||
except:
|
||||
assert False, "Error processing file %s" % path
|
||||
commands.convert(input_file=path, major_mode="doc")
|
||||
except Exception as e:
|
||||
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:
|
||||
tempdir.cleanup()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,38 +1,41 @@
|
||||
import unittest
|
||||
|
||||
from ptulsconv.docparser.tag_compiler import Event
|
||||
from ptulsconv.docparser.adr_entity import ADRLine, make_entity
|
||||
from fractions import Fraction
|
||||
|
||||
from ptulsconv.docparser.adr_entity import ADRLine, make_entity
|
||||
from ptulsconv.docparser.tag_compiler import Event
|
||||
|
||||
|
||||
class TestADREntity(unittest.TestCase):
|
||||
def test_event2line(self):
|
||||
tags = {
|
||||
'Ver': '1.0',
|
||||
'Actor': "Bill",
|
||||
'CN': "1",
|
||||
'QN': 'J1001',
|
||||
'R': 'Noise',
|
||||
'EFF': 'EFF'
|
||||
"Ver": "1.0",
|
||||
"Actor": "Bill",
|
||||
"CN": "1",
|
||||
"QN": "J1001",
|
||||
"R": "Noise",
|
||||
"EFF": "EFF",
|
||||
}
|
||||
event = Event(clip_name='"This is a test." (sotto voce)',
|
||||
event = Event(
|
||||
clip_name='"This is a test." (sotto voce)',
|
||||
track_name="Justin",
|
||||
session_name="Test Project",
|
||||
tags=tags,
|
||||
start=Fraction(0, 1), finish=Fraction(1, 1))
|
||||
start=Fraction(0, 1),
|
||||
finish=Fraction(1, 1),
|
||||
)
|
||||
|
||||
line = make_entity(event)
|
||||
|
||||
self.assertIsInstance(line, ADRLine)
|
||||
self.assertEqual('Bill', line.actor_name)
|
||||
self.assertEqual('Justin', line.character_name)
|
||||
self.assertEqual("Bill", line.actor_name)
|
||||
self.assertEqual("Justin", line.character_name)
|
||||
self.assertEqual('"This is a test." (sotto voce)', line.prompt)
|
||||
self.assertEqual('Noise', line.reason)
|
||||
self.assertEqual('J1001', line.cue_number)
|
||||
self.assertEqual("Noise", line.reason)
|
||||
self.assertEqual("J1001", line.cue_number)
|
||||
self.assertEqual(True, line.effort)
|
||||
self.assertEqual('Test Project', line.title)
|
||||
self.assertEqual('1.0', line.version)
|
||||
self.assertEqual("Test Project", line.title)
|
||||
self.assertEqual("1.0", line.version)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import unittest
|
||||
from ptulsconv import broadcast_timecode
|
||||
from fractions import Fraction
|
||||
|
||||
from ptulsconv import broadcast_timecode
|
||||
|
||||
|
||||
class TestBroadcastTimecode(unittest.TestCase):
|
||||
def test_basic_to_frame_count(self):
|
||||
r1 = "01:00:00:00"
|
||||
@@ -66,7 +68,9 @@ class TestBroadcastTimecode(unittest.TestCase):
|
||||
def test_seconds_to_smpte(self):
|
||||
secs = Fraction(25, 24)
|
||||
frame_duration = Fraction(1, 24)
|
||||
tc_format = broadcast_timecode.TimecodeFormat(frame_duration=frame_duration, logical_fps=24, drop_frame=False)
|
||||
tc_format = broadcast_timecode.TimecodeFormat(
|
||||
frame_duration=frame_duration, logical_fps=24, drop_frame=False
|
||||
)
|
||||
s1 = tc_format.seconds_to_smpte(secs)
|
||||
self.assertEqual(s1, "00:00:01:01")
|
||||
|
||||
@@ -77,9 +81,11 @@ class TestBroadcastTimecode(unittest.TestCase):
|
||||
|
||||
def test_unparseable_timecode(self):
|
||||
time_str = "11.32-19"
|
||||
s1 = broadcast_timecode.smpte_to_frame_count(time_str, frames_per_logical_second=24)
|
||||
s1 = broadcast_timecode.smpte_to_frame_count(
|
||||
time_str, frames_per_logical_second=24
|
||||
)
|
||||
self.assertIsNone(s1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import unittest
|
||||
from ptulsconv.docparser.doc_entity import HeaderDescriptor
|
||||
from fractions import Fraction
|
||||
|
||||
from ptulsconv.docparser.doc_entity import HeaderDescriptor
|
||||
|
||||
|
||||
class DocParserTestCase(unittest.TestCase):
|
||||
|
||||
def test_header(self):
|
||||
header = HeaderDescriptor(session_name="Test Session",
|
||||
header = HeaderDescriptor(
|
||||
session_name="Test Session",
|
||||
sample_rate=48000.0,
|
||||
bit_depth=24,
|
||||
start_timecode="00:59:52:00",
|
||||
@@ -14,11 +15,12 @@ class DocParserTestCase(unittest.TestCase):
|
||||
timecode_drop_frame=False,
|
||||
count_audio_tracks=0,
|
||||
count_clips=0,
|
||||
count_files=0)
|
||||
count_files=0,
|
||||
)
|
||||
|
||||
self.assertEqual(header.session_name, "Test Session")
|
||||
self.assertEqual(header.start_time, Fraction((59 * 60 + 52) * 30, 30))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import unittest
|
||||
|
||||
from ptulsconv import footage
|
||||
|
||||
|
||||
class TestFootage(unittest.TestCase):
|
||||
def test_basic_footage(self):
|
||||
r1 = "90+0"
|
||||
@@ -11,5 +13,3 @@ class TestFootage(unittest.TestCase):
|
||||
r1 = "1+8"
|
||||
f1 = footage.footage_to_seconds(r1)
|
||||
self.assertEqual(float(f1 or 0), 1.0)
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import unittest
|
||||
from fractions import Fraction
|
||||
|
||||
import ptulsconv.docparser.tag_compiler
|
||||
from ptulsconv.docparser import doc_entity
|
||||
from fractions import Fraction
|
||||
|
||||
|
||||
class TestTagCompiler(unittest.TestCase):
|
||||
|
||||
def test_one_track(self):
|
||||
c = ptulsconv.docparser.tag_compiler.TagCompiler()
|
||||
|
||||
@@ -16,31 +15,48 @@ class TestTagCompiler(unittest.TestCase):
|
||||
|
||||
events = c.compile_events()
|
||||
event1 = next(events)
|
||||
self.assertEqual('This is clip 1', event1.clip_name)
|
||||
self.assertEqual('Track 1', event1.track_name)
|
||||
self.assertEqual('Test Session', event1.session_name)
|
||||
self.assertEqual(dict(A='A',
|
||||
Color='Blue',
|
||||
Ver='1.1',
|
||||
Mode='2',
|
||||
Comment='This is some text in the comments',
|
||||
Part='1'), event1.tags)
|
||||
self.assertEqual("This is clip 1", event1.clip_name)
|
||||
self.assertEqual("Track 1", event1.track_name)
|
||||
self.assertEqual("Test Session", event1.session_name)
|
||||
self.assertEqual(
|
||||
dict(
|
||||
A="A",
|
||||
Color="Blue",
|
||||
Ver="1.1",
|
||||
Mode="2",
|
||||
Comment="This is some text in the comments",
|
||||
Part="1",
|
||||
),
|
||||
event1.tags,
|
||||
)
|
||||
self.assertEqual(Fraction(3600, 1), event1.start)
|
||||
|
||||
event2 = next(events)
|
||||
self.assertEqual("This is the second clip ...and this is the last clip", event2.clip_name)
|
||||
self.assertEqual('Track 1', event2.track_name)
|
||||
self.assertEqual('Test Session', event2.session_name)
|
||||
self.assertEqual(dict(R='Noise', A='A', B='B',
|
||||
Color='Red',
|
||||
Comment='This is some text in the comments',
|
||||
N='1', Mode='2',
|
||||
Ver='1.1',
|
||||
M1='M1',
|
||||
Part='2'), event2.tags)
|
||||
self.assertEqual(
|
||||
"This is the second clip ...and this is the last clip", event2.clip_name
|
||||
)
|
||||
self.assertEqual("Track 1", event2.track_name)
|
||||
self.assertEqual("Test Session", event2.session_name)
|
||||
self.assertEqual(
|
||||
dict(
|
||||
R="Noise",
|
||||
A="A",
|
||||
B="B",
|
||||
Color="Red",
|
||||
Comment="This is some text in the comments",
|
||||
N="1",
|
||||
Mode="2",
|
||||
Ver="1.1",
|
||||
M1="M1",
|
||||
Part="2",
|
||||
),
|
||||
event2.tags,
|
||||
)
|
||||
|
||||
self.assertEqual(c.session.header.convert_timecode('01:00:01:10'), event2.start)
|
||||
self.assertEqual(c.session.header.convert_timecode('01:00:03:00'), event2.finish)
|
||||
self.assertEqual(c.session.header.convert_timecode("01:00:01:10"), event2.start)
|
||||
self.assertEqual(
|
||||
c.session.header.convert_timecode("01:00:03:00"), event2.finish
|
||||
)
|
||||
|
||||
self.assertIsNone(next(events, None))
|
||||
|
||||
@@ -51,74 +67,94 @@ class TestTagCompiler(unittest.TestCase):
|
||||
|
||||
all_tags = c.compile_tag_list()
|
||||
|
||||
self.assertTrue(all_tags['Mode'] == {'2', '1'})
|
||||
self.assertTrue(all_tags["Mode"] == {"2", "1"})
|
||||
|
||||
@staticmethod
|
||||
def make_test_session():
|
||||
test_header = doc_entity.HeaderDescriptor(session_name="Test Session $Ver=1.1",
|
||||
test_header = doc_entity.HeaderDescriptor(
|
||||
session_name="Test Session $Ver=1.1",
|
||||
sample_rate=48000,
|
||||
timecode_format="24",
|
||||
timecode_drop_frame=False,
|
||||
bit_depth=24,
|
||||
start_timecode='00:59:00:00',
|
||||
start_timecode="00:59:00:00",
|
||||
count_audio_tracks=1,
|
||||
count_clips=3,
|
||||
count_files=0
|
||||
count_files=0,
|
||||
)
|
||||
test_clips = [
|
||||
doc_entity.TrackClipDescriptor(channel=1, event=1,
|
||||
clip_name='This is clip 1 {Color=Blue} $Mode=2',
|
||||
start_time='01:00:00:00',
|
||||
finish_time='01:00:01:03',
|
||||
duration='00:00:01:03',
|
||||
state='Unmuted',
|
||||
timestamp=None),
|
||||
doc_entity.TrackClipDescriptor(channel=1, event=2,
|
||||
clip_name='This is the second clip {R=Noise} [B] $Mode=2',
|
||||
start_time='01:00:01:10',
|
||||
finish_time='01:00:02:00',
|
||||
duration='00:00:00:14',
|
||||
state='Unmuted',
|
||||
timestamp=None),
|
||||
doc_entity.TrackClipDescriptor(channel=1, event=3,
|
||||
clip_name='& ...and this is the last clip $N=1 $Mode=2',
|
||||
start_time='01:00:02:00',
|
||||
finish_time='01:00:03:00',
|
||||
duration='00:00:01:00',
|
||||
state='Unmuted',
|
||||
timestamp=None),
|
||||
doc_entity.TrackClipDescriptor(
|
||||
channel=1,
|
||||
event=1,
|
||||
clip_name="This is clip 1 {Color=Blue} $Mode=2",
|
||||
start_time="01:00:00:00",
|
||||
finish_time="01:00:01:03",
|
||||
duration="00:00:01:03",
|
||||
state="Unmuted",
|
||||
timestamp=None,
|
||||
),
|
||||
doc_entity.TrackClipDescriptor(
|
||||
channel=1,
|
||||
event=2,
|
||||
clip_name="This is the second clip {R=Noise} [B] $Mode=2",
|
||||
start_time="01:00:01:10",
|
||||
finish_time="01:00:02:00",
|
||||
duration="00:00:00:14",
|
||||
state="Unmuted",
|
||||
timestamp=None,
|
||||
),
|
||||
doc_entity.TrackClipDescriptor(
|
||||
channel=1,
|
||||
event=3,
|
||||
clip_name="& ...and this is the last clip $N=1 $Mode=2",
|
||||
start_time="01:00:02:00",
|
||||
finish_time="01:00:03:00",
|
||||
duration="00:00:01:00",
|
||||
state="Unmuted",
|
||||
timestamp=None,
|
||||
),
|
||||
]
|
||||
test_track = doc_entity.TrackDescriptor(name="Track 1 [A] {Color=Red} $Mode=1",
|
||||
test_track = doc_entity.TrackDescriptor(
|
||||
index=0,
|
||||
name="Track 1 [A] {Color=Red} $Mode=1",
|
||||
comments="{Comment=This is some text in the comments}",
|
||||
user_delay_samples=0,
|
||||
plugins=[],
|
||||
state=[],
|
||||
clips=test_clips)
|
||||
clips=test_clips,
|
||||
)
|
||||
|
||||
markers = [doc_entity.MarkerDescriptor(number=1,
|
||||
markers = [
|
||||
doc_entity.MarkerDescriptor(
|
||||
number=1,
|
||||
location="01:00:00:00",
|
||||
time_reference=48000 * 3600,
|
||||
units="Samples",
|
||||
name="Marker 1 {Part=1}",
|
||||
comments=""
|
||||
comments="",
|
||||
track_marker=False,
|
||||
),
|
||||
doc_entity.MarkerDescriptor(number=2,
|
||||
doc_entity.MarkerDescriptor(
|
||||
number=2,
|
||||
location="01:00:01:00",
|
||||
time_reference=48000 * 3601,
|
||||
units="Samples",
|
||||
name="Marker 2 {Part=2}",
|
||||
comments="[M1]"
|
||||
comments="[M1]",
|
||||
track_marker=False,
|
||||
),
|
||||
]
|
||||
|
||||
test_session = doc_entity.SessionDescriptor(header=test_header,
|
||||
test_session = doc_entity.SessionDescriptor(
|
||||
header=test_header,
|
||||
tracks=[test_track],
|
||||
clips=[],
|
||||
files=[],
|
||||
markers=markers,
|
||||
plugins=[])
|
||||
plugins=[],
|
||||
)
|
||||
return test_session
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from ptulsconv.docparser.tagged_string_parser_visitor import parse_tags, TagPreModes
|
||||
from ptulsconv.docparser.tagged_string_parser_visitor import TagPreModes, parse_tags
|
||||
|
||||
|
||||
class TestTagInterpreter(unittest.TestCase):
|
||||
@@ -17,12 +17,12 @@ class TestTagInterpreter(unittest.TestCase):
|
||||
|
||||
def test_tags(self):
|
||||
s1 = parse_tags("{a=100}")
|
||||
self.assertEqual(s1.tag_dict['a'], "100")
|
||||
self.assertEqual(s1.tag_dict["a"], "100")
|
||||
|
||||
s2 = parse_tags("{b=This is a test} [option] $X=9")
|
||||
self.assertEqual(s2.tag_dict['b'], 'This is a test')
|
||||
self.assertEqual(s2.tag_dict['option'], 'option')
|
||||
self.assertEqual(s2.tag_dict['X'], "9")
|
||||
self.assertEqual(s2.tag_dict["b"], "This is a test")
|
||||
self.assertEqual(s2.tag_dict["option"], "option")
|
||||
self.assertEqual(s2.tag_dict["X"], "9")
|
||||
|
||||
def test_modes(self):
|
||||
s1 = parse_tags("@ Monday Tuesday {a=1}")
|
||||
@@ -35,5 +35,5 @@ class TestTagInterpreter(unittest.TestCase):
|
||||
self.assertEqual(s3.mode, TagPreModes.APPEND)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import unittest
|
||||
from ptulsconv.docparser import doc_entity, pt_doc_parser, tag_compiler
|
||||
import os.path
|
||||
import unittest
|
||||
|
||||
from ptulsconv.docparser import doc_entity, pt_doc_parser, tag_compiler
|
||||
|
||||
|
||||
class TaggingIntegratedTests(unittest.TestCase):
|
||||
path = os.path.dirname(__file__) + '/../export_cases/Tag Tests/Tag Tests.txt'
|
||||
path = os.path.dirname(__file__) + "/../export_cases/Tag Tests/Tag Tests.txt"
|
||||
|
||||
def test_event_list(self):
|
||||
with open(self.path, 'r') as f:
|
||||
with open(self.path, "r") as f:
|
||||
document_ast = pt_doc_parser.protools_text_export_grammar.parse(f.read())
|
||||
document: doc_entity.SessionDescriptor = pt_doc_parser.DocParserVisitor().visit(document_ast)
|
||||
document: doc_entity.SessionDescriptor = (
|
||||
pt_doc_parser.DocParserVisitor().visit(document_ast)
|
||||
)
|
||||
compiler = tag_compiler.TagCompiler()
|
||||
compiler.session = document
|
||||
|
||||
@@ -27,9 +30,11 @@ class TaggingIntegratedTests(unittest.TestCase):
|
||||
self.assertEqual("Region 04", events[8].clip_name)
|
||||
|
||||
def test_append(self):
|
||||
with open(self.path, 'r') as f:
|
||||
with open(self.path, "r") as f:
|
||||
document_ast = pt_doc_parser.protools_text_export_grammar.parse(f.read())
|
||||
document: doc_entity.SessionDescriptor = pt_doc_parser.DocParserVisitor().visit(document_ast)
|
||||
document: doc_entity.SessionDescriptor = (
|
||||
pt_doc_parser.DocParserVisitor().visit(document_ast)
|
||||
)
|
||||
compiler = tag_compiler.TagCompiler()
|
||||
compiler.session = document
|
||||
|
||||
@@ -39,20 +44,26 @@ class TaggingIntegratedTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual("Dolor sic amet the rain in spain", events[2].clip_name)
|
||||
|
||||
self.assertEqual(document.header.convert_timecode("01:00:10:00"), events[2].start)
|
||||
self.assertEqual(document.header.convert_timecode("01:00:25:00"), events[2].finish)
|
||||
self.assertEqual(
|
||||
document.header.convert_timecode("01:00:10:00"), events[2].start
|
||||
)
|
||||
self.assertEqual(
|
||||
document.header.convert_timecode("01:00:25:00"), events[2].finish
|
||||
)
|
||||
|
||||
self.assertIn('X', events[2].tags.keys())
|
||||
self.assertIn('ABC', events[2].tags.keys())
|
||||
self.assertIn('A', events[2].tags.keys())
|
||||
self.assertEqual('302', events[2].tags['X'])
|
||||
self.assertEqual('ABC', events[2].tags['ABC'])
|
||||
self.assertEqual('1', events[2].tags['A'])
|
||||
self.assertIn("X", events[2].tags.keys())
|
||||
self.assertIn("ABC", events[2].tags.keys())
|
||||
self.assertIn("A", events[2].tags.keys())
|
||||
self.assertEqual("302", events[2].tags["X"])
|
||||
self.assertEqual("ABC", events[2].tags["ABC"])
|
||||
self.assertEqual("1", events[2].tags["A"])
|
||||
|
||||
def test_successive_appends(self):
|
||||
with open(self.path, 'r') as f:
|
||||
with open(self.path, "r") as f:
|
||||
document_ast = pt_doc_parser.protools_text_export_grammar.parse(f.read())
|
||||
document: doc_entity.SessionDescriptor = pt_doc_parser.DocParserVisitor().visit(document_ast)
|
||||
document: doc_entity.SessionDescriptor = (
|
||||
pt_doc_parser.DocParserVisitor().visit(document_ast)
|
||||
)
|
||||
compiler = tag_compiler.TagCompiler()
|
||||
compiler.session = document
|
||||
|
||||
@@ -62,9 +73,13 @@ class TaggingIntegratedTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual("A B C", events[3].clip_name)
|
||||
|
||||
self.assertEqual(document.header.convert_timecode("01:00:15:00"), events[3].start)
|
||||
self.assertEqual(document.header.convert_timecode("01:00:45:00"), events[3].finish)
|
||||
self.assertEqual(
|
||||
document.header.convert_timecode("01:00:15:00"), events[3].start
|
||||
)
|
||||
self.assertEqual(
|
||||
document.header.convert_timecode("01:00:45:00"), events[3].finish
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -8,13 +8,13 @@ class MyTestCase(unittest.TestCase):
|
||||
v = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
expected = [1, 2, 7, 5, 6, 15, 9, 10]
|
||||
|
||||
should = (lambda x, y: y % 4 == 0)
|
||||
do_combine = (lambda x, y: x + y)
|
||||
should = lambda x, y: y % 4 == 0
|
||||
do_combine = lambda x, y: x + y
|
||||
|
||||
r = apply_appends(iter(v), should, do_combine)
|
||||
r1 = list(r)
|
||||
self.assertEqual(r1, expected)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user