61 Commits

Author SHA1 Message Date
Jamie Hardt
09ed12fc8f Migrating to uv build and manager (#16)
* Migrating to uv build and manager

* Updated Flake8 settings

* Updating github workflow

* Update python-package.yml

* Removed redundant flake8 run

* Version number in banner

---------

Co-authored-by: Jamie Hardt <jamie@squad51.us>
2025-09-20 10:04:15 -07:00
4cd6ba1772 Updated for latest ptsl-py
Major version change, dependency changes
2025-09-19 09:52:24 -07:00
1942b323b3 Merge branch 'master' of https://github.com/iluvcapra/ptulsconv 2025-09-08 16:27:19 -07:00
7d297a7564 Nudged version 2025-09-08 16:26:40 -07:00
Jamie Hardt
54fa8f04a7 Update pyproject.toml
Nudge version to 3.0.0
2025-09-08 13:18:24 -07:00
Jamie Hardt
dcc6113a63 Merge pull request #15 from iluvcapra/remove-3.8-support
Remove 3.8 Support
2025-09-08 13:14:47 -07:00
Jamie Hardt
04b9e35240 Update python-package.yml
Remove 3.8 from test matrix
2025-09-08 13:13:09 -07:00
Jamie Hardt
f460022160 Update pyproject.toml
Removing 3.8 from classifiers
2025-09-08 13:11:23 -07:00
5c5cd84811 Merged upstream 2025-09-08 13:06:02 -07:00
Jamie Hardt
ee90697be0 Update pythonpublish.yml
pypa/gh-action-pypi-publish@v1.13.0
2025-09-08 12:50:46 -07:00
Jamie Hardt
4a0d19ade1 Added 3.13 to classifiers. 2025-05-25 07:58:55 -07:00
Jamie Hardt
df6c783c51 Added 3.13 to test matrix 2025-05-25 07:57:42 -07:00
Jamie Hardt
f0b232b2b6 autopep 2025-05-25 07:54:50 -07:00
Jamie Hardt
519c6403ba Nudged version 2025-05-25 07:52:11 -07:00
Jamie Hardt
d29c36eafa Fixed some bugs I introduced, fixed entrypoint 2025-05-25 07:50:37 -07:00
Jamie Hardt
2095a1fb75 Nudged version 2025-05-25 07:24:07 -07:00
Jamie Hardt
70defcc46c fixed typo in copyright line 2025-05-25 07:23:25 -07:00
Jamie Hardt
d156b6df89 Added notes 2025-05-25 07:21:50 -07:00
Jamie Hardt
3ba9d7933e Merge branch 'master' of https://github.com/iluvcapra/ptulsconv 2025-05-25 07:16:45 -07:00
Jamie Hardt
b0c40ee0b6 Merged 2025-05-25 07:16:35 -07:00
Jamie Hardt
921b0f07af Update pyproject.toml
Fixing sphinx dependencies
2025-05-25 07:13:11 -07:00
Jamie Hardt
57764bc859 Update conf.py 2025-05-25 07:05:24 -07:00
Jamie Hardt
779c93282c Update conf.py
Updated copyright message
2025-05-25 07:04:17 -07:00
Jamie Hardt
9684be6c7e Update __init__.py 2025-05-25 07:03:28 -07:00
Jamie Hardt
484a70fc8e Update __init__.py 2025-05-25 07:01:47 -07:00
Jamie Hardt
5aa005c317 Update conf.py 2025-05-25 07:00:44 -07:00
Jamie Hardt
454adea3d1 Merge pull request #13 from iluvcapra/maint-poetry
Upgrade build tool to Poetry
2025-05-24 22:25:53 -07:00
Jamie Hardt
1e6546dab5 Tweak file for flake 2025-05-24 22:24:45 -07:00
Jamie Hardt
8b262d3bfb Rearranged pyproject, brought in metadata 2025-05-24 22:22:04 -07:00
Jamie Hardt
630e7960dc Making changes for peotry 2025-05-24 22:20:15 -07:00
Jamie Hardt
aa7b418121 Update __init__.py
Nudging version to 2.2.1
2025-05-24 21:58:50 -07:00
Jamie Hardt
a519a525b2 Update pythonpublish.yml
Updating python publish action to the latest version
2025-05-24 21:54:42 -07:00
Jamie Hardt
1412efe509 autopep 2025-05-18 13:39:06 -07:00
Jamie Hardt
12a6c05467 autopep 2025-05-18 13:37:46 -07:00
Jamie Hardt
cf87986014 autopep'd test 2025-05-18 13:35:12 -07:00
Jamie Hardt
67533879f8 Rewrote parsing to handle old & new-style markers 2025-05-18 13:33:51 -07:00
Jamie Hardt
f847b88aa3 Nudged version and copyright date 2025-05-17 12:06:56 -07:00
Jamie Hardt
c3a600c5d7 Integrated track marker test case and fixed parser 2025-05-17 12:05:27 -07:00
Jamie Hardt
914783a809 Updated documentation 2025-05-17 11:26:07 -07:00
Jamie Hardt
c638c673e8 Adding track marker export case 2025-05-17 11:23:54 -07:00
Jamie Hardt
15fe6667af Fixed up unit test 2025-05-17 11:23:02 -07:00
Jamie Hardt
d4e23b59eb Adding support for track markers
(Always ignore for now)
2025-05-17 11:19:22 -07:00
Jamie Hardt
a602b09551 flake8 2025-05-17 10:47:21 -07:00
Jamie Hardt
448d93d717 Fix for flake 2025-05-17 10:45:40 -07:00
Jamie Hardt
59e7d40d97 Merge branch 'master' of https://github.com/iluvcapra/ptulsconv 2025-05-11 22:19:26 -07:00
Jamie Hardt
eaa5fe824f Fixed parser logic to handle new-style marker tracks 2025-05-11 22:17:42 -07:00
Jamie Hardt
8ebfd32e02 Update __init__.py
Nudge version
2024-07-10 21:16:45 -07:00
Jamie Hardt
83a9adb48a Merge remote-tracking branch 'refs/remotes/origin/master' 2023-11-15 23:09:27 -08:00
Jamie Hardt
013ebcbe75 movie options 2023-11-15 23:09:12 -08:00
Jamie Hardt
c87695e5fe Merge pull request #12 from iluvcapra/maint-py312
Add Python 3.12 support
2023-11-08 10:33:13 -08:00
Jamie Hardt
4a8983cbbb Update python-package.yml
Added 3.12 to test matrix
2023-11-08 10:27:56 -08:00
Jamie Hardt
9123cbd0b5 Update pyproject.toml
Added 3.12 classifier
2023-11-08 10:27:04 -08:00
Jamie Hardt
4224d106b0 Fixed compyright notice 2023-11-04 11:50:03 -07:00
Jamie Hardt
ac22fab97f Some style fixes (all E231) 2023-11-04 11:36:34 -07:00
Jamie Hardt
64ca2c6c5c Silenced some more errors 2023-11-04 11:23:40 -07:00
Jamie Hardt
c3af30dc6a Renamed my JSONEncoder something useful 2023-11-04 11:21:59 -07:00
Jamie Hardt
c30f675cec Cleared up a type warning 2023-11-04 11:17:48 -07:00
Jamie Hardt
204af7d9cb A bunch of typo cleanups and styling. 2023-11-04 11:13:49 -07:00
Jamie Hardt
10fc211e80 Some typos 2023-11-04 10:56:44 -07:00
Jamie Hardt
d56c7df376 Updated documentation to reflect current usage
No longer have to output a text export.
Some formatting changes.
2023-11-04 10:49:56 -07:00
Jamie Hardt
7b38449a5f Fixed formatting of a list. 2023-11-04 10:43:21 -07:00
38 changed files with 249 additions and 160 deletions

View File

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

View File

@@ -16,7 +16,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: [3.8, 3.9, "3.10", "3.11"] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v2.5.0 - uses: actions/checkout@v2.5.0
@@ -32,10 +32,9 @@ jobs:
- name: Lint with flake8 - name: Lint with flake8
run: | run: |
# stop the build if there are Python syntax errors or undefined names # stop the build if there are Python syntax errors or undefined names
flake8 ptulsconv tests --count --select=E9,F63,F7,F82 --show-source --statistics flake8 src/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 # exit-zero treats all errors as warnings.
flake8 ptulsconv tests --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics flake8 src/ptulsconv tests --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest - name: Test with pytest
run: | run: |
pytest pytest
flake8 ptulsconv

View File

@@ -26,14 +26,4 @@ jobs:
- name: Build package - name: Build package
run: python -m build run: python -m build
- name: pypi-publish - name: pypi-publish
uses: pypa/gh-action-pypi-publish@v1.8.6 uses: pypa/gh-action-pypi-publish@v1.13.0
# - name: Report to Mastodon
# uses: cbrgm/mastodon-github-action@v1.0.1
# with:
# message: |
# I just released a new version of ptulsconv, my ADR cue sheet generator!
# #python #protools #pdf #filmmaking
# ${{ github.server_url }}/${{ github.repository }}
# env:
# MASTODON_URL: ${{ secrets.MASTODON_URL }}
# MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,52 +1,52 @@
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"
[project] [project]
name = "ptulsconv" name = "ptulsconv"
authors = [ version = "4.0.0"
{name = "Jamie Hardt", email = "jamiehardt@me.com"}, description = "Read Pro Tools Text exports and generate PDF ADR Reports, JSON"
]
readme = "README.md" readme = "README.md"
requires-python = ">=3.9"
license = { file = "LICENSE" } license = { file = "LICENSE" }
keywords = ["text-processing", "parsers", "film",
"broadcast", "editing", "editorial"]
classifiers = [ classifiers = [
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Topic :: Multimedia', 'Topic :: Multimedia',
'Topic :: Multimedia :: Sound/Audio', 'Topic :: Multimedia :: Sound/Audio',
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Topic :: Text Processing :: Filters" "Topic :: Text Processing :: Filters"
] ]
requires-python = ">=3.8" authors = [{name = "Jamie Hardt", email = "<jamiehardt@me.com>"}]
dynamic = ["version", "description"]
keywords = ["text-processing", "parsers", "film",
"broadcast", "editing", "editorial"]
dependencies = [ dependencies = [
'parsimonious', "parsimonious>=0.10.0",
'tqdm', "py-ptsl>=600.0.0",
'reportlab', "reportlab>=4.4.4",
'py-ptsl >= 101.1.0' "tqdm>=4.67.1",
]
[project.optional-dependencies]
doc = [
"Sphinx ~= 5.3.0",
"sphinx-rtd-theme >= 1.1.1"
] ]
[tool.flit.module]
name = "ptulsconv"
[project.scripts]
ptulsconv = "ptulsconv.__main__:main"
[project.entry_points.console_scripts]
ptulsconv = 'ptulsconv.__main__:main'
[project.urls] [project.urls]
Source = 'https://github.com/iluvcapra/ptulsconv' Source = 'https://github.com/iluvcapra/ptulsconv'
Issues = 'https://github.com/iluvcapra/ptulsconv/issues' Issues = 'https://github.com/iluvcapra/ptulsconv/issues'
Documentation = 'https://ptulsconv.readthedocs.io/' Documentation = 'https://ptulsconv.readthedocs.io/'
[project.optional-dependencies]
doc = [
"sphinx>=7.4.7",
"sphinx-rtd-theme>=3.0.2",
]
[project.scripts]
ptulsconv = "ptulsconv:__main__.main"
[build-system]
requires = ["uv_build>=0.8.18,<0.9.0"]
build-backend = "uv_build"
[dependency-groups]
dev = [
"flake8>=7.3.0",
]

View File

@@ -0,0 +1,5 @@
"""
Parse and convert Pro Tools text exports
"""
__copyright__ = "ptulsconv (c) 2025 Jamie Hardt. All rights reserved."

View File

@@ -2,7 +2,10 @@ from optparse import OptionParser, OptionGroup
import datetime import datetime
import sys import sys
from ptulsconv import __name__, __copyright__ import importlib.metadata
from ptulsconv import __name__
import ptulsconv
from ptulsconv.commands import convert from ptulsconv.commands import convert
from ptulsconv.reporting import print_status_style, \ from ptulsconv.reporting import print_status_style, \
print_banner_style, print_section_header_style, \ print_banner_style, print_section_header_style, \
@@ -41,6 +44,11 @@ def main():
default='doc', default='doc',
help='Set output format, `raw`, `tagged`, `doc`.') help='Set output format, `raw`, `tagged`, `doc`.')
parser.add_option('-m', '--movie-opts',
dest='movie_opts',
metavar="MOVIE_OPTS",
help="Set movie options")
warn_options = OptionGroup(title="Warning and Validation Options", warn_options = OptionGroup(title="Warning and Validation Options",
parser=parser) parser=parser)
@@ -76,8 +84,10 @@ def main():
'and exit.') 'and exit.')
parser.add_option_group(informational_options) parser.add_option_group(informational_options)
print_banner_style(__copyright__) 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) (options, args) = parser.parse_args(sys.argv)

View File

@@ -14,9 +14,9 @@ from fractions import Fraction
import ptsl import ptsl
from .docparser.adr_entity import make_entities, ADRLine from .docparser.adr_entity import make_entities, ADRLine
from .reporting import print_section_header_style, print_status_style,\ from .reporting import print_section_header_style, print_status_style, \
print_warning print_warning
from .validations import validate_unique_field, validate_non_empty_field,\ from .validations import validate_unique_field, validate_non_empty_field, \
validate_dependent_value validate_dependent_value
from ptulsconv.docparser import parse_document from ptulsconv.docparser import parse_document
@@ -32,7 +32,7 @@ from ptulsconv.pdf.continuity import output_report as output_continuity
from json import JSONEncoder from json import JSONEncoder
class MyEncoder(JSONEncoder): class FractionEncoder(JSONEncoder):
""" """
A subclass of :class:`JSONEncoder` which encodes :class:`Fraction` objects A subclass of :class:`JSONEncoder` which encodes :class:`Fraction` objects
as a dict. as a dict.
@@ -97,7 +97,7 @@ def output_adr_csv(lines: List[ADRLine], time_format: TimecodeFormat):
os.chdir("..") os.chdir("..")
def generate_documents(session_tc_format, scenes, adr_lines: Iterator[ADRLine], def generate_documents(session_tc_format, scenes, adr_lines: List[ADRLine],
title): title):
""" """
Create PDF output. Create PDF output.
@@ -112,7 +112,7 @@ def generate_documents(session_tc_format, scenes, adr_lines: Iterator[ADRLine],
supervisor = next((x.supervisor for x in adr_lines), "") supervisor = next((x.supervisor for x in adr_lines), "")
output_continuity(scenes=scenes, tc_display_format=session_tc_format, output_continuity(scenes=scenes, tc_display_format=session_tc_format,
title=title, client=client, title=title, client=client or "",
supervisor=supervisor) supervisor=supervisor)
reels = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6'] reels = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6']
@@ -193,7 +193,7 @@ def convert(major_mode, input_file=None, output=sys.stdout, warnings=True):
session_tc_format = session.header.timecode_format session_tc_format = session.header.timecode_format
if major_mode == 'raw': if major_mode == 'raw':
output.write(MyEncoder().encode(session)) output.write(FractionEncoder().encode(session))
else: else:
compiler = TagCompiler() compiler = TagCompiler()
@@ -201,7 +201,7 @@ def convert(major_mode, input_file=None, output=sys.stdout, warnings=True):
compiled_events = list(compiler.compile_events()) compiled_events = list(compiler.compile_events())
if major_mode == 'tagged': if major_mode == 'tagged':
output.write(MyEncoder().encode(compiled_events)) output.write(FractionEncoder().encode(compiled_events))
elif major_mode == 'doc': elif major_mode == 'doc':
generic_events, adr_lines = make_entities(compiled_events) generic_events, adr_lines = make_entities(compiled_events)
@@ -225,9 +225,10 @@ def convert(major_mode, input_file=None, output=sys.stdout, warnings=True):
print_status_style("%i ADR events found." % len(adr_lines)) print_status_style("%i ADR events found." % len(adr_lines))
if warnings: if warnings:
perform_adr_validations(adr_lines) perform_adr_validations(iter(adr_lines))
generate_documents(session_tc_format, scenes, adr_lines, title) generate_documents(session_tc_format, scenes, adr_lines,
title)
def perform_adr_validations(lines: Iterator[ADRLine]): def perform_adr_validations(lines: Iterator[ADRLine]):

View File

@@ -19,11 +19,17 @@ class SessionDescriptor:
self.tracks = kwargs['tracks'] self.tracks = kwargs['tracks']
self.markers = kwargs['markers'] self.markers = kwargs['markers']
def markers_timed(self) -> Iterator[Tuple['MarkerDescriptor', Fraction]]: def markers_timed(self,
only_ruler_markers: bool = True) -> \
Iterator[Tuple['MarkerDescriptor', Fraction]]:
""" """
Iterate each marker in the session with its respective time reference. Iterate each marker in the session with its respective time reference.
""" """
for marker in self.markers: for marker in self.markers:
if marker.track_marker and only_ruler_markers:
continue
marker_time = Fraction(marker.time_reference, marker_time = Fraction(marker.time_reference,
int(self.header.sample_rate)) int(self.header.sample_rate))
# marker_time = self.header.convert_timecode(marker.location) # marker_time = self.header.convert_timecode(marker.location)
@@ -182,6 +188,7 @@ class MarkerDescriptor:
units: str units: str
name: str name: str
comments: str comments: str
track_marker: bool
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.number = kwargs['number'] self.number = kwargs['number']
@@ -190,3 +197,4 @@ class MarkerDescriptor:
self.units = kwargs['units'] self.units = kwargs['units']
self.name = kwargs['name'] self.name = kwargs['name']
self.comments = kwargs['comments'] self.comments = kwargs['comments']
self.track_marker = kwargs['track_marker']

View File

@@ -1,15 +1,16 @@
from parsimonious.nodes import NodeVisitor from parsimonious.nodes import NodeVisitor
from parsimonious.grammar import Grammar from parsimonious.grammar import Grammar
from .doc_entity import SessionDescriptor, HeaderDescriptor, TrackDescriptor,\ from .doc_entity import SessionDescriptor, HeaderDescriptor, TrackDescriptor, \
FileDescriptor, TrackClipDescriptor, ClipDescriptor, PluginDescriptor,\ FileDescriptor, TrackClipDescriptor, ClipDescriptor, PluginDescriptor, \
MarkerDescriptor MarkerDescriptor
protools_text_export_grammar = Grammar( protools_text_export_grammar = Grammar(
r""" r"""
document = header files_section? clips_section? plugin_listing? document = header files_section? clips_section? plugin_listing?
track_listing? markers_listing? track_listing? markers_block?
header = "SESSION NAME:" fs string_value rs header = "SESSION NAME:" fs string_value rs
"SAMPLE RATE:" fs float_value rs "SAMPLE RATE:" fs float_value rs
"BIT DEPTH:" fs integer_value "-bit" rs "BIT DEPTH:" fs integer_value "-bit" rs
@@ -74,17 +75,33 @@ protools_text_export_grammar = Grammar(
track_clip_state = ("Muted" / "Unmuted") track_clip_state = ("Muted" / "Unmuted")
markers_listing = markers_listing_header markers_column_header markers_block = markers_block_header
marker_record* (markers_list / markers_list_simple)
markers_listing_header = "M A R K E R S L I S T I N G" rs
markers_column_header = "# " fs "LOCATION " fs markers_list_simple = markers_column_header_simple marker_record_simple*
"TIME REFERENCE " fs
"UNITS " fs markers_list = markers_column_header marker_record*
"NAME " fs
"COMMENTS" rs markers_block_header = "M A R K E R S L I S T I N G" rs
markers_column_header_simple =
"# LOCATION TIME REFERENCE "
"UNITS NAME "
"COMMENTS" rs
markers_column_header =
"# LOCATION TIME REFERENCE "
"UNITS NAME "
"TRACK NAME "
"TRACK TYPE COMMENTS" rs
marker_record_simple = integer_value isp fs string_value fs
integer_value isp fs string_value fs string_value
fs string_value rs
marker_record = integer_value isp fs string_value fs integer_value isp fs marker_record = integer_value isp fs string_value fs integer_value isp fs
string_value fs string_value fs string_value rs string_value fs string_value fs string_value fs
string_value fs string_value rs
fs = "\t" fs = "\t"
rs = "\n" rs = "\n"
@@ -231,22 +248,37 @@ class DocParserVisitor(NodeVisitor):
return node.text return node.text
@staticmethod @staticmethod
def visit_markers_listing(_, visited_children): def visit_markers_block(_, visited_children):
markers = [] markers = []
for marker in visited_children[2]: for marker in visited_children[1][0][1]:
markers.append(marker) markers.append(marker)
return markers return markers
@staticmethod @staticmethod
def visit_marker_record(_, visited_children): def visit_marker_record_simple(_, visited_children):
return MarkerDescriptor(number=visited_children[0], return MarkerDescriptor(number=visited_children[0],
location=visited_children[3], location=visited_children[3],
time_reference=visited_children[5], time_reference=visited_children[5],
units=visited_children[8], units=visited_children[8],
name=visited_children[10], name=visited_children[10],
comments=visited_children[12]) comments=visited_children[12],
track_marker=False)
@staticmethod
def visit_marker_record(_, visited_children):
track_type = visited_children[15]
is_track_marker = (track_type == "Track")
return MarkerDescriptor(number=visited_children[0],
location=visited_children[3],
time_reference=visited_children[5],
units=visited_children[8],
name=visited_children[10],
comments=visited_children[16],
track_marker=is_track_marker)
@staticmethod @staticmethod
def visit_formatted_clip_name(_, visited_children): def visit_formatted_clip_name(_, visited_children):

View File

@@ -5,7 +5,7 @@ from .__init__ import make_doc_template
from reportlab.lib.units import inch from reportlab.lib.units import inch
from reportlab.lib.pagesizes import letter from reportlab.lib.pagesizes import letter
from reportlab.platypus import Paragraph, Spacer, KeepTogether, Table,\ from reportlab.platypus import Paragraph, Spacer, KeepTogether, Table, \
HRFlowable HRFlowable
from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors from reportlab.lib import colors

View File

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

View File

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

View File

@@ -102,14 +102,14 @@ class TestTagCompiler(unittest.TestCase):
time_reference=48000 * 3600, time_reference=48000 * 3600,
units="Samples", units="Samples",
name="Marker 1 {Part=1}", name="Marker 1 {Part=1}",
comments="" comments="", track_marker=False,
), ),
doc_entity.MarkerDescriptor(number=2, doc_entity.MarkerDescriptor(number=2,
location="01:00:01:00", location="01:00:01:00",
time_reference=48000 * 3601, time_reference=48000 * 3601,
units="Samples", units="Samples",
name="Marker 2 {Part=2}", name="Marker 2 {Part=2}",
comments="[M1]" comments="[M1]", track_marker=False,
), ),
] ]