45 Commits

Author SHA1 Message Date
Jamie Hardt
10c0e4f038 Fixed regex statements in parser
This clears up a bunch of `DeprecationWarning`s in pytest
2022-11-06 12:25:25 -08:00
Jamie Hardt
fc2e823116 Merge branch 'master' of https://github.com/iluvcapra/ptulsconv 2022-11-06 11:54:03 -08:00
Jamie Hardt
fbc7531374 Include version string in all outputs 2022-11-06 11:54:00 -08:00
Jamie Hardt
1fb17b13ea Update pythonpublish.yml
Updated to API key publishing method instead of username-password, which is deprecated.
2022-11-06 11:15:53 -08:00
Jamie Hardt
21c32e282c Updated version and banner 2022-11-04 18:03:38 -07:00
Jamie Hardt
8407d31333 Update adr_entity.py 2022-11-04 18:03:29 -07:00
Jamie Hardt
97d6eeda02 Bugfixes/linted 2022-11-04 13:34:38 -07:00
Jamie Hardt
3bee7a8391 Update CONTRIBUTING.md 2022-11-04 13:23:04 -07:00
Jamie Hardt
68d38f8ed5 Create CONTRIBUTING.md 2022-11-04 13:22:39 -07:00
Jamie Hardt
8e043b7175 Added footage decode featue 2022-11-04 13:19:08 -07:00
Jamie Hardt
a7b5adfffb Bug fixes 2022-11-04 13:06:38 -07:00
Jamie Hardt
da5b743191 Create coc-settings.json 2022-11-04 12:50:20 -07:00
Jamie Hardt
caa5381306 Merge branch 'master' of https://github.com/iluvcapra/ptulsconv 2022-11-04 12:50:13 -07:00
Jamie Hardt
9e2b932cad Update broadcast_timecode.py
Subtle bug fixes
2022-11-04 12:50:11 -07:00
Jamie Hardt
05ea48078f Fixed a bug, talent sides weren't sorting by time 2022-09-04 14:36:13 -07:00
Jamie Hardt
c26fa8dd75 Update summary_log.py
Added more metadata readout
2022-08-17 22:21:26 -07:00
Jamie Hardt
9f8e3cf824 Update line_count.py
Fixed a bug with omitted cues
2022-08-17 22:21:16 -07:00
Jamie Hardt
3b438b1399 Merge branch 'master' of https://github.com/iluvcapra/ptulsconv 2022-08-17 21:32:56 -07:00
Jamie Hardt
41b1a3185f Update __init__.py
Nudge version
2022-08-17 21:32:53 -07:00
Jamie Hardt
8877982a47 Update summary_log.py
Add "OMITTED" to ADR summary pdf
2022-08-17 21:32:45 -07:00
Jamie Hardt
bb6fbcfd37 Update __main__.py
Print more fields to --available-tags
2022-08-17 21:32:31 -07:00
Jamie Hardt
434b8816ee Merge branch 'master' of https://github.com/iluvcapra/ptulsconv 2022-06-27 12:06:55 -07:00
Jamie Hardt
5ebaf6b473 Updated requirements.txt 2022-06-27 12:06:03 -07:00
Jamie Hardt
d0f415b38f Create workspace.xml 2022-01-16 17:14:30 -08:00
Jamie Hardt
c5d6d82831 Merge branch 'master' of https://github.com/iluvcapra/ptulsconv 2022-01-16 17:14:23 -08:00
Jamie Hardt
66a71283d5 Futura has bee purged 2022-01-11 14:53:19 -08:00
Jamie Hardt
15ad328edc Begin testing text files 2022-01-11 14:26:38 -08:00
Jamie Hardt
a48eccb0d0 Cleaned up some regexps 2022-01-11 14:24:17 -08:00
Jamie Hardt
fa2cef35b2 Fixed an error in the tag compiler test 2022-01-11 14:05:21 -08:00
Jamie Hardt
c8053f65ae Reorganized trst sources 2022-01-11 13:56:44 -08:00
Jamie Hardt
d9da7317a7 Added lcov.info 2022-01-11 12:54:08 -08:00
Jamie Hardt
ab614cbc32 Commented out line 2022-01-11 12:53:50 -08:00
Jamie Hardt
5a75a77f77 Reorganized tests a little 2022-01-11 12:33:01 -08:00
Jamie Hardt
17f17d592a Nudged version to 0.8.3 2022-01-11 12:15:23 -08:00
Jamie Hardt
b41d92b842 Merge branch 'master' of https://github.com/iluvcapra/ptulsconv 2022-01-11 12:14:47 -08:00
Jamie Hardt
dc11a40530 Update setup.py
Added 3.10
2022-01-11 12:14:11 -08:00
Jamie Hardt
b80e593350 Update python-package.yml 2022-01-11 12:12:44 -08:00
Jamie Hardt
961d68df21 Update python-package.yml
Added 3.10 to python versions
2022-01-11 12:11:45 -08:00
Jamie Hardt
4daa5f0496 Bumped to Python 3.9 2021-12-24 12:47:45 -08:00
Jamie Hardt
de48bcfe24 Added comment 2021-12-24 12:46:44 -08:00
Jamie Hardt
80729443d1 Tweaked typo 2021-10-04 10:54:43 -07:00
Jamie Hardt
c1671f3656 Twiddles, testing Working Copy 2021-10-04 10:47:14 -07:00
Jamie Hardt
6c76827f42 Update README.md 2021-10-02 19:21:13 -07:00
Jamie Hardt
1228e2adbe Manpage 0.8.2 bump 2021-10-02 17:42:09 -07:00
Jamie Hardt
ccf6e65210 removed files 2021-09-29 11:58:22 -07:00
50 changed files with 379 additions and 345 deletions

View File

@@ -16,7 +16,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: [3.7, 3.8, 3.9] python-version: [3.7, 3.8, 3.9, "3.10"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@@ -22,8 +22,8 @@ jobs:
pip install parsimonious pip install parsimonious
- name: Build and publish - name: Build and publish
env: env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} TWINE_PASSWORD: ${{ secrets.PYPI_UPLOAD_API_KEY }}
run: | run: |
python setup.py sdist bdist_wheel python setup.py sdist bdist_wheel
twine upload dist/* twine upload dist/*

1
.gitignore vendored
View File

@@ -104,3 +104,4 @@ venv.bak/
.mypy_cache/ .mypy_cache/
.DS_Store .DS_Store
/example/Charade/Session File Backups/ /example/Charade/Session File Backups/
lcov.info

4
.idea/.gitignore generated vendored
View File

@@ -1,4 +0,0 @@
# Default ignored files
/workspace.xml
/tasks.xml

View File

@@ -1,19 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="jamie">
<words>
<w>adlib</w>
<w>bottompadding</w>
<w>fmpxml</w>
<w>futura</w>
<w>leftpadding</w>
<w>lineafter</w>
<w>linebefore</w>
<w>ptulsconv</w>
<w>retval</w>
<w>smpte</w>
<w>subheader</w>
<w>timecode</w>
<w>timespan</w>
</words>
</dictionary>
</component>

View File

@@ -1,9 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="jamiehardt">
<words>
<w>fmpxmlresult</w>
<w>frac</w>
<w>mins</w>
</words>
</dictionary>
</component>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

10
.idea/misc.xml generated
View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (ptulsconv)" project-jdk-type="Python SDK" />
<component name="PyPackaging">
<option name="earlyReleasesAsUpgrades" value="true" />
</component>
<component name="PythonCompatibilityInspectionAdvertiser">
<option name="version" value="3" />
</component>
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ptulsconv.iml" filepath="$PROJECT_DIR$/.idea/ptulsconv.iml" />
</modules>
</component>
</project>

10
.idea/ptulsconv.iml generated
View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.8 (ptulsconv)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

66
.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="68bdb183-5bdf-4b42-962e-28e58c31a89d" name="Default Changelist" comment="">
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/ptulsconv.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/ptulsconv.iml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitSEFilterConfiguration">
<file-type-list>
<filtered-out-file-type name="LOCAL_BRANCH" />
<filtered-out-file-type name="REMOTE_BRANCH" />
<filtered-out-file-type name="TAG" />
<filtered-out-file-type name="COMMIT_BY_MESSAGE" />
</file-type-list>
</component>
<component name="ProjectId" id="1yyIGrXKNUCYtI4PSaCWGoLG76R" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
<option name="showMembers" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="project-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="68bdb183-5bdf-4b42-962e-28e58c31a89d" name="Default Changelist" comment="" />
<created>1633217312285</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1633217312285</updated>
</task>
<task id="LOCAL-00001" summary="Reorganized README a little">
<created>1633221191797</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1633221191797</updated>
</task>
<task id="LOCAL-00002" summary="Manpage 0.8.2 bump">
<created>1633221729867</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1633221729867</updated>
</task>
<option name="localTasksCounter" value="3" />
<servers />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Reorganized README a little" />
<MESSAGE value="Manpage 0.8.2 bump" />
<option name="LAST_COMMIT_MESSAGE" value="Manpage 0.8.2 bump" />
</component>
</project>

5
.vim/coc-settings.json Normal file
View File

@@ -0,0 +1,5 @@
{
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.linting.mypyEnabled": false
}

9
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,9 @@
# Contributing to ptulsconv
## Testing
Before submitting PRs or patches, please make sure your branch passes all of the unit tests by running Pytest.
```sh
~/ptulsconv$ pytest
```

View File

@@ -1,7 +1,6 @@
![](https://img.shields.io/github/license/iluvcapra/ptulsconv.svg) ![](https://img.shields.io/github/license/iluvcapra/ptulsconv.svg)
![](https://img.shields.io/pypi/pyversions/ptulsconv.svg) ![](https://img.shields.io/pypi/pyversions/ptulsconv.svg)
[![](https://img.shields.io/pypi/v/ptulsconv.svg)][pypi] [![](https://img.shields.io/pypi/v/ptulsconv.svg)][pypi]
![](https://img.shields.io/pypi/wheel/ptulsconv.svg)
![Lint and Test](https://github.com/iluvcapra/ptulsconv/actions/workflows/python-package.yml/badge.svg) ![Lint and Test](https://github.com/iluvcapra/ptulsconv/actions/workflows/python-package.yml/badge.svg)
[pypi]: https://pypi.org/project/ptulsconv/ [pypi]: https://pypi.org/project/ptulsconv/
@@ -44,77 +43,6 @@ workflows.
[avp]: http://www.avid.com/pro-tools [avp]: http://www.avid.com/pro-tools
### Adding Detailed Info to Clip Names with Fields
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]`
The row output for this clip will contain columns for the values:
|...| PT.Clip.Name | note | V | FX | DESIGN | ... |
|---|--------------------|-------------------|---|----|--------|-----|
|...| Fireworks explosion| Replace for final | 1 | FX | DESIGN | ... |
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 character 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 two clips are named:
`"Squad fifty-one, what is your status?" [FUTZ] {Ch=Dispatcher} [ADR]`
`"We are ten-eight at Rampart Hospital." {Ch=Gage} [ADR]`
The output will contain the range:
|...| PT.Clip.Name| Ch | FUTZ | ADR | ...|
|---|------------|------|---|----|-----|
|...| "Squad fifty-one, what is your status?"| Dispatcher | FUTZ | ADR | ... |
|...| "We are ten-eight at Rampart Hospital."| Gage | | ADR | ... |
### 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
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.
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 prevails.
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
track, the value set on the clip will prevail.
### Using `@` to Apply Fields to a Span of Clips
A clip name beginning with "@" will not be included in the CSV 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.
### Using `&` to Combine Clips
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 (later 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 become the start time of the row, and the finish time of the last clip
will become the finish time of the row.
## Installation ## Installation

View File

@@ -1,38 +1,18 @@
.\" Manpage for ptulsconv .\" Manpage for ptulsconv
.\" Contact https://github.com/iluvcapra/ptulsconv .\" Contact https://github.com/iluvcapra/ptulsconv
.TH ptulsconv 1 "15 May 2020" "0.4.0" "ptulsconv man page" .TH ptulsconv 1 "15 May 2020" "0.8.2" "ptulsconv man page"
.SH NAME .SH NAME
.BR "ptulsconv" " \- convert .BR "ptulsconv" " \- convert
.IR "Avid Pro Tools" " text exports" .IR "Avid Pro Tools" " text exports"
.SH SYNOPSIS .SH SYNOPSIS
ptulsconv [OPTIONS] Export.txt ptulsconv [OPTIONS] Export.txt
.SH DESCRIPTION .SH DESCRIPTION
Convert a Pro Tools text export into a flat list of clip names with timecodes. A tagging Convert a Pro Tools text export into ADR reports.
language is interpreted to add columns and type the data. The default output format is
an XML file for import into Filemaker Pro.
.SH OPTIONS .SH OPTIONS
.IP "-h, --help" .IP "-h, --help"
show a help message and exit. show a help message and exit.
.TP .TP
.RI "-i " "TC"
Drop events before this timecode.
.TP
.RI "-o " "TC"
Drop events after this timecode.
.TP
.RI "-m "
Include muted clips.
.TP
.RI "--json "
Output a JSON document instead of XML. (--xform will have no effect.)
.TP
.RI "--xform " "NAME"
Convert the output with a built-in output transform.
.TP
.RI "--show-available-tags" .RI "--show-available-tags"
Print a list of tags that are interpreted and exit. Print a list of tags that are interpreted and exit.
.TP
.RI "--show-available-transforms"
Print a list of built-in output transforms and exit.
.SH AUTHOR .SH AUTHOR
Jamie Hardt (contact at https://github.com/iluvcapra/ptulsconv) Jamie Hardt (contact at https://github.com/iluvcapra/ptulsconv)

View File

@@ -1,5 +1,6 @@
from ptulsconv.docparser.ptuls_grammar import protools_text_export_grammar from ptulsconv.docparser.ptuls_grammar import protools_text_export_grammar
__version__ = '0.8.1' __version__ = '1.0.1'
__author__ = 'Jamie Hardt' __author__ = 'Jamie Hardt'
__license__ = 'MIT' __license__ = 'MIT'
__copyright__ = "%s %s (c) 2022 %s. All rights reserved." % (__name__, __version__, __author__)

View File

@@ -2,7 +2,7 @@ from optparse import OptionParser, OptionGroup
import datetime import datetime
import sys import sys
from ptulsconv import __name__, __version__, __author__ from ptulsconv import __name__, __version__, __author__,__copyright__
from ptulsconv.commands import convert from ptulsconv.commands import convert
from ptulsconv.reporting import print_status_style, print_banner_style, print_section_header_style, print_fatal_error from ptulsconv.reporting import print_status_style, print_banner_style, print_section_header_style, print_fatal_error
@@ -17,8 +17,9 @@ from ptulsconv.reporting import print_status_style, print_banner_style, print_se
def dump_field_map(output=sys.stdout): def dump_field_map(output=sys.stdout):
from ptulsconv.docparser.tag_mapping import TagMapping from ptulsconv.docparser.tag_mapping import TagMapping
from ptulsconv.docparser.adr_entity import ADRLine from ptulsconv.docparser.adr_entity import ADRLine, GenericEvent
TagMapping.print_rules(GenericEvent, output=output)
TagMapping.print_rules(ADRLine, output=output) TagMapping.print_rules(ADRLine, output=output)
@@ -58,9 +59,10 @@ def main():
parser.add_option_group(informational_options) parser.add_option_group(informational_options)
print_banner_style(__copyright__)
(options, args) = parser.parse_args(sys.argv) (options, args) = parser.parse_args(sys.argv)
print_banner_style("%s %s (c) 2021 %s. All rights reserved." % (__name__, __version__, __author__))
print_section_header_style("Startup") print_section_header_style("Startup")
print_status_style("This run started %s" % (datetime.datetime.now().isoformat())) print_status_style("This run started %s" % (datetime.datetime.now().isoformat()))

View File

@@ -2,20 +2,23 @@ from fractions import Fraction
import re import re
import math import math
from collections import namedtuple from collections import namedtuple
from typing import Optional, SupportsFloat
class TimecodeFormat(namedtuple("_TimecodeFormat", "frame_duration logical_fps drop_frame")): class TimecodeFormat(namedtuple("_TimecodeFormat", "frame_duration logical_fps drop_frame")):
def smpte_to_seconds(self, smpte: str) -> Fraction: def smpte_to_seconds(self, smpte: str) -> Optional[Fraction]:
frame_count = smpte_to_frame_count(smpte, self.logical_fps, drop_frame_hint=self.drop_frame) frame_count = smpte_to_frame_count(smpte, self.logical_fps, drop_frame_hint=self.drop_frame)
return frame_count * self.frame_duration if frame_count is None:
return None
else:
return frame_count * self.frame_duration
def seconds_to_smpte(self, seconds: Fraction) -> str: def seconds_to_smpte(self, seconds: SupportsFloat) -> str:
frame_count = int(seconds / self.frame_duration) frame_count = int(seconds / self.frame_duration)
return frame_count_to_smpte(frame_count, self.logical_fps, self.drop_frame) return frame_count_to_smpte(frame_count, self.logical_fps, self.drop_frame)
def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int, drop_frame_hint=False) -> int: def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int, drop_frame_hint=False) -> Optional[int]:
""" """
Convert a string with a SMPTE timecode representation into a frame count. Convert a string with a SMPTE timecode representation into a frame count.
@@ -28,7 +31,11 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int,
""" """
assert frames_per_logical_second in [24, 25, 30, 48, 50, 60] assert frames_per_logical_second in [24, 25, 30, 48, 50, 60]
m = re.search("(\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
hh, mm, ss, sep, ff, frac = m.groups() hh, mm, ss, sep, ff, frac = m.groups()
hh, mm, ss, ff, frac = int(hh), int(mm), int(ss), int(ff), float(frac or 0.0) hh, mm, ss, ff, frac = int(hh), int(mm), int(ss), int(ff), float(frac or 0.0)
@@ -47,14 +54,14 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int,
frames_dropped_per_inst = (frames_per_logical_second / 15) frames_dropped_per_inst = (frames_per_logical_second / 15)
mins = hh * 60 + mm mins = hh * 60 + mm
inst_count = mins - math.floor(mins / 10) inst_count = mins - math.floor(mins / 10)
dropped_frames = frames_dropped_per_inst * inst_count dropped_frames = int(frames_dropped_per_inst) * inst_count
frames = raw_frames - dropped_frames frames = raw_frames - dropped_frames
return frames return frames
def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, drop_frame: bool = False, def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, drop_frame: bool = False,
fractional_frame: float = None) -> str: fractional_frame: Optional[float] = None) -> str:
assert frames_per_logical_second in [24, 25, 30, 48, 50, 60] assert frames_per_logical_second in [24, 25, 30, 48, 50, 60]
assert fractional_frame is None or fractional_frame < 1.0 assert fractional_frame is None or fractional_frame < 1.0
@@ -80,8 +87,10 @@ def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, drop_
return "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff) return "%02i:%02i:%02i%s%02i" % (hh, mm, ss, separator, ff)
def footage_to_frame_count(footage_string): def footage_to_frame_count(footage_string) -> Optional[int]:
m = re.search("(\d+)\+(\d+)(\.\d+)?", footage_string) m = re.search(r'(\d+)\+(\d+)(\.\d+)?', footage_string)
if m is None:
return None
feet, frm, frac = m.groups() feet, frm, frac = m.groups()
feet, frm, frac = int(feet), int(frm), float(frac or 0.0) feet, frm, frac = int(feet), int(frm), float(frac or 0.0)

View File

@@ -5,6 +5,7 @@ import sys
from itertools import chain from itertools import chain
import csv import csv
from typing import List from typing import List
from fractions import Fraction
from .docparser.adr_entity import make_entities from .docparser.adr_entity import make_entities
from .reporting import print_section_header_style, print_status_style, print_warning from .reporting import print_section_header_style, print_status_style, print_warning
@@ -59,10 +60,12 @@ def output_adr_csv(lines: List[ADRLine], time_format: TimecodeFormat):
'Reason', 'Note', 'TV']) 'Reason', 'Note', 'TV'])
for event in these_lines: 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, this_row = [event.title, event.character_name, event.cue_number,
event.reel, event.version, event.reel, event.version,
time_format.seconds_to_smpte(event.start), time_format.seconds_to_smpte(event.finish), time_format.seconds_to_smpte(this_start), time_format.seconds_to_smpte(this_finish),
float(event.start), float(event.finish), float(this_start), float(this_finish),
event.prompt, event.prompt,
event.reason, event.note, "TV" if event.tv else ""] event.reason, event.note, "TV" if event.tv else ""]
@@ -126,7 +129,7 @@ def create_adr_reports(lines: List[ADRLine], tc_display_format: TimecodeFormat,
# return parsed # return parsed
def convert(input_file, major_mode='fmpxml', output=sys.stdout, warnings=True): def convert(input_file, major_mode, output=sys.stdout, warnings=True):
session = parse_document(input_file) session = parse_document(input_file)
session_tc_format = session.header.timecode_format session_tc_format = session.header.timecode_format

View File

@@ -1,5 +1,5 @@
from ptulsconv.docparser.tag_compiler import Event from ptulsconv.docparser.tag_compiler import Event
from typing import Optional, List, Tuple, Any from typing import Optional, List, Tuple
from dataclasses import dataclass from dataclasses import dataclass
from fractions import Fraction from fractions import Fraction
@@ -11,12 +11,10 @@ def make_entities(from_events: List[Event]) -> Tuple[List['GenericEvent'], List[
adr_lines = list() adr_lines = list()
for event in from_events: for event in from_events:
result: Any = make_entity(event) result = make_entity(event)
if type(result) is ADRLine: if type(result) is ADRLine:
result: ADRLine
adr_lines.append(result) adr_lines.append(result)
elif type(result) is GenericEvent: elif type(result) is GenericEvent:
result: GenericEvent
generic_events.append(result) generic_events.append(result)
return generic_events, adr_lines return generic_events, adr_lines
@@ -41,17 +39,17 @@ def make_entity(from_event: Event) -> Optional[object]:
@dataclass @dataclass
class GenericEvent: class GenericEvent:
title: Optional[str] title: str = ""
supervisor: Optional[str] supervisor: Optional[str] = None
client: Optional[str] client: Optional[str] = None
scene: Optional[str] scene: Optional[str] = None
version: Optional[str] version: Optional[str] = None
reel: Optional[str] reel: Optional[str] = None
start: Optional[Fraction] start: Fraction = Fraction(0,1)
finish: Optional[Fraction] finish: Fraction = Fraction(0,1)
omitted: bool omitted: bool = False
note: Optional[str] note: Optional[str] = None
requested_by: Optional[str] requested_by: Optional[str] = None
tag_mapping = [ tag_mapping = [
TagMapping(source='Title', target="title", alt=TagMapping.ContentSource.Session), TagMapping(source='Title', target="title", alt=TagMapping.ContentSource.Session),
@@ -69,21 +67,21 @@ class GenericEvent:
@dataclass @dataclass
class ADRLine(GenericEvent): class ADRLine(GenericEvent):
priority: Optional[int] priority: Optional[int] = None
cue_number: Optional[str] cue_number: Optional[str] = None
character_id: Optional[str] character_id: Optional[str] = None
character_name: Optional[str] character_name: Optional[str] = None
actor_name: Optional[str] actor_name: Optional[str] = None
prompt: Optional[str] prompt: Optional[str] = None
reason: Optional[str] reason: Optional[str] = None
time_budget_mins: Optional[float] time_budget_mins: Optional[float] = None
spot: Optional[str] spot: Optional[str] = None
shot: Optional[str] shot: Optional[str] = None
effort: bool effort: bool = False
tv: bool tv: bool = False
tbw: bool tbw: bool = False
adlib: bool adlib: bool = False
optional: bool optional: bool = False
tag_mapping = [ tag_mapping = [
@@ -111,30 +109,30 @@ class ADRLine(GenericEvent):
formatter=(lambda x: len(x) > 0)) formatter=(lambda x: len(x) > 0))
] ]
def __init__(self): # def __init__(self):
self.title = None # self.title = None
self.supervisor = None # self.supervisor = None
self.client = None # self.client = None
self.scene = None # self.scene = None
self.version = None # self.version = None
self.reel = None # self.reel = None
self.start = None # self.start = None
self.finish = None # self.finish = None
self.priority = None # self.priority = None
self.cue_number = None # self.cue_number = None
self.character_id = None # self.character_id = None
self.character_name = None # self.character_name = None
self.actor_name = None # self.actor_name = None
self.prompt = None # self.prompt = None
self.reason = None # self.reason = None
self.requested_by = None # self.requested_by = None
self.time_budget_mins = None # self.time_budget_mins = None
self.note = None # self.note = None
self.spot = None # self.spot = None
self.shot = None # self.shot = None
self.effort = False # self.effort = False
self.tv = False # self.tv = False
self.tbw = False # self.tbw = False
self.omitted = False # self.omitted = False
self.adlib = False # self.adlib = False
self.optional = False # self.optional = False

View File

@@ -21,7 +21,8 @@ class SessionDescriptor:
def markers_timed(self) -> Iterator[Tuple['MarkerDescriptor', Fraction]]: def markers_timed(self) -> Iterator[Tuple['MarkerDescriptor', Fraction]]:
for marker in self.markers: for marker in self.markers:
marker_time = self.header.convert_timecode(marker.location) marker_time = Fraction(marker.time_reference, int(self.header.sample_rate))
#marker_time = self.header.convert_timecode(marker.location)
yield marker, marker_time yield marker, marker_time
def tracks_clips(self) -> Iterator[Tuple['TrackDescriptor', 'TrackClipDescriptor']]: def tracks_clips(self) -> Iterator[Tuple['TrackDescriptor', 'TrackClipDescriptor']]:

View File

@@ -67,8 +67,8 @@ protools_text_export_grammar = Grammar(
fs = "\t" fs = "\t"
rs = "\n" rs = "\n"
block_ending = rs rs block_ending = rs rs
string_value = ~"[^\t\n]*" string_value = ~r"[^\t\n]*"
integer_value = ~"\d+" integer_value = ~r"\d+"
float_value = ~"\d+(\.\d+)?" float_value = ~r"\d+(\.\d+)?"
isp = ~"[^\d\t\n]*" isp = ~r"[^\d\t\n]*"
""") """)

View File

@@ -1,4 +1,3 @@
import sys
from collections import namedtuple from collections import namedtuple
from fractions import Fraction from fractions import Fraction
from typing import Iterator, Tuple, Callable, Generator, Dict, List from typing import Iterator, Tuple, Callable, Generator, Dict, List
@@ -72,9 +71,9 @@ class TagCompiler:
def _marker_tags(self, at): def _marker_tags(self, at):
retval = dict() retval = dict()
applicable = [(m, t) for (m, t) in self.session.markers_timed() if t <= at] applicable = [(m, t) for (m, t) in self.session.markers_timed() if t <= at]
for marker, time in sorted(applicable, key=lambda x: x[1]): for marker, _ in sorted(applicable, key=lambda x: x[1]):
retval.update(parse_tags(marker.comments).tag_dict) retval.update(parse_tags(marker.comments or "").tag_dict)
retval.update(parse_tags(marker.name).tag_dict) retval.update(parse_tags(marker.name or "").tag_dict)
return retval return retval

View File

@@ -18,24 +18,24 @@ tag_grammar = Grammar(
key_tag = "[" key "]" word_sep? key_tag = "[" key "]" word_sep?
short_tag = "$" key "=" word word_sep? short_tag = "$" key "=" word word_sep?
full_text_tag = "{" key "=" value "}" word_sep? full_text_tag = "{" key "=" value "}" word_sep?
key = ~"[A-Za-z][A-Za-z0-9_]*" key = ~r"[A-Za-z][A-Za-z0-9_]*"
value = ~"[^}]+" value = ~r"[^}]+"
tag_junk = word word_sep? tag_junk = word word_sep?
word = ~"[^ \[\{\$][^ ]*" word = ~r"[^ \[\{\$][^ ]*"
word_sep = ~" +" word_sep = ~r" +"
modifier = ("@" / "&") word_sep? modifier = ("@" / "&") word_sep?
""" """
) )
def parse_tags(prompt) -> "TaggedStringResult": def parse_tags(prompt: str) -> "TaggedStringResult":
ast = tag_grammar.parse(prompt) ast = tag_grammar.parse(prompt)
return TagListVisitor().visit(ast) return TagListVisitor().visit(ast)
class TaggedStringResult: class TaggedStringResult:
content: Optional[str] content: str
tag_dict: Optional[Dict[str, str]] tag_dict: Dict[str, str]
mode: TagPreModes mode: TagPreModes
def __init__(self, content, tag_dict, mode): def __init__(self, content, tag_dict, mode):

18
ptulsconv/footage.py Normal file
View File

@@ -0,0 +1,18 @@
from fractions import Fraction
import re
from typing import Optional
def footage_to_seconds(footage: str) -> Optional[Fraction]:
m = re.match(r'(\d+)\+(\d+)(\.\d+)?', footage)
if m is None:
return None
feet, frames, _ = m.groups()
feet, frames = int(feet), int(frames)
fps = 24
frames_per_foot = 16
total_frames = feet * frames_per_foot + frames
return Fraction(total_frames, fps)

View File

@@ -1,4 +1,4 @@
import ffmpeg # ffmpeg-python #import ffmpeg # ffmpeg-python
# TODO: Implement movie export # TODO: Implement movie export

View File

@@ -9,9 +9,11 @@ from reportlab.platypus.frames import Frame
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfbase.ttfonts import TTFont
from typing import List
# TODO: A Generic report useful for spotting # TODO: A Generic report useful for spotting
# TODO: A report useful for M&E mixer's notes # TODO: A report useful for M&E mixer's notes
# TODO: Use a default font that doesn't need to be installed
# This is from https://code.activestate.com/recipes/576832/ for # This is from https://code.activestate.com/recipes/576832/ for
# generating page count messages # generating page count messages
@@ -36,7 +38,7 @@ class ReportCanvas(canvas.Canvas):
def draw_page_number(self, page_count): def draw_page_number(self, page_count):
self.saveState() self.saveState()
self.setFont("Futura", 10) self.setFont('Helvetica', 10) #FIXME make this customizable
self.drawString(0.5 * inch, 0.5 * inch, "Page %d of %d" % (self._pageNumber, page_count)) self.drawString(0.5 * inch, 0.5 * inch, "Page %d of %d" % (self._pageNumber, page_count))
right_edge = self._pagesize[0] - 0.5 * inch right_edge = self._pagesize[0] - 0.5 * inch
self.drawRightString(right_edge, 0.5 * inch, self._report_date.strftime("%m/%d/%Y %H:%M")) self.drawRightString(right_edge, 0.5 * inch, self._report_date.strftime("%m/%d/%Y %H:%M"))
@@ -60,7 +62,8 @@ def make_doc_template(page_size, filename, document_title,
document_header: str, document_header: str,
client: str, client: str,
document_subheader: str, document_subheader: str,
left_margin=0.5 * inch) -> ADRDocTemplate: left_margin=0.5 * inch,
fonts: List[TTFont] = []) -> ADRDocTemplate:
right_margin = top_margin = bottom_margin = 0.5 * inch right_margin = top_margin = bottom_margin = 0.5 * inch
page_box = GRect(0., 0., page_size[0], page_size[1]) page_box = GRect(0., 0., page_size[0], page_size[1])
_, page_box = page_box.split_x(left_margin, direction='l') _, page_box = page_box.split_x(left_margin, direction='l')
@@ -72,15 +75,22 @@ def make_doc_template(page_size, filename, document_title,
header_box, page_box = page_box.split_y(0.75 * inch, direction='d') header_box, page_box = page_box.split_y(0.75 * inch, direction='d')
title_box, report_box = header_box.split_x(3.5 * inch, direction='r') title_box, report_box = header_box.split_x(3.5 * inch, direction='r')
page_template = PageTemplate(id="Main", on_page_lambda = (lambda c, _:
frames=[Frame(page_box.min_x, page_box.min_y, page_box.width, page_box.height)], draw_header_footer(c, report_box, title_box,
onPage=lambda c, _: draw_header_footer(c, report_box, title_box, footer_box, footer_box,title=title,
title=title, supervisor=supervisor, supervisor=supervisor,
document_subheader=document_subheader, document_subheader=document_subheader,
client=client, client=client, doc_title=document_header))
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)
for font in fonts:
pdfmetrics.registerFont(font)
pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
doc = ADRDocTemplate(filename, doc = ADRDocTemplate(filename,
title=document_title, title=document_title,
author=supervisor, author=supervisor,
@@ -94,6 +104,11 @@ def make_doc_template(page_size, filename, document_title,
def time_format(mins, zero_str="-"): def time_format(mins, zero_str="-"):
"""
Formats a duration `mins` into a string
"""
if mins is None:
return zero_str
if mins == 0. and zero_str is not None: if mins == 0. and zero_str is not None:
return zero_str return zero_str
elif mins < 60.: elif mins < 60.:
@@ -105,11 +120,11 @@ def time_format(mins, zero_str="-"):
def draw_header_footer(a_canvas: ReportCanvas, left_box, right_box, footer_box, title: str, supervisor: str, def draw_header_footer(a_canvas: ReportCanvas, left_box, right_box, footer_box, title: str, supervisor: str,
document_subheader: str, client: str, doc_title=""): document_subheader: str, client: str, doc_title="", font_name='Helvetica'):
(_supervisor_box, client_box,), title_box = right_box.divide_y([16., 16., ]) (_supervisor_box, client_box,), title_box = right_box.divide_y([16., 16., ])
title_box.draw_text_cell(a_canvas, title, "Futura", 18, inset_y=2., inset_x=5.) title_box.draw_text_cell(a_canvas, title, font_name, 18, inset_y=2., inset_x=5.)
client_box.draw_text_cell(a_canvas, client, "Futura", 11, inset_y=2., inset_x=5.) client_box.draw_text_cell(a_canvas, client, font_name, 11, inset_y=2., inset_x=5.)
a_canvas.saveState() a_canvas.saveState()
a_canvas.setLineWidth(0.5) a_canvas.setLineWidth(0.5)
@@ -126,13 +141,13 @@ def draw_header_footer(a_canvas: ReportCanvas, left_box, right_box, footer_box,
(doc_title_cell, spotting_version_cell,), _ = left_box.divide_y([18., 14], direction='d') (doc_title_cell, spotting_version_cell,), _ = left_box.divide_y([18., 14], direction='d')
doc_title_cell.draw_text_cell(a_canvas, doc_title, 'Futura', 14., inset_y=2.) doc_title_cell.draw_text_cell(a_canvas, doc_title, font_name, 14., inset_y=2.)
if document_subheader is not None: if document_subheader is not None:
spotting_version_cell.draw_text_cell(a_canvas, document_subheader, 'Futura', 12., inset_y=2.) spotting_version_cell.draw_text_cell(a_canvas, document_subheader, font_name, 12., inset_y=2.)
if supervisor is not None: if supervisor is not None:
a_canvas.setFont('Futura', 11.) a_canvas.setFont(font_name, 11.)
a_canvas.drawCentredString(footer_box.min_x + footer_box.width / 2., footer_box.min_y, supervisor) a_canvas.drawCentredString(footer_box.min_x + footer_box.width / 2., footer_box.min_y, supervisor)

View File

@@ -12,9 +12,9 @@ from ptulsconv.pdf import make_doc_template
# TODO: A Continuity # TODO: A Continuity
def table_for_scene(scene, tc_format): def table_for_scene(scene, tc_format, font_name = 'Helvetica'):
scene_style = getSampleStyleSheet()['Normal'] scene_style = getSampleStyleSheet()['Normal']
scene_style.fontName = 'Futura' scene_style.fontName = font_name
scene_style.leftIndent = 0. scene_style.leftIndent = 0.
scene_style.leftPadding = 0. scene_style.leftPadding = 0.
scene_style.spaceAfter = 18. scene_style.spaceAfter = 18.
@@ -29,7 +29,7 @@ def table_for_scene(scene, tc_format):
style = [('VALIGN', (0, 0), (-1, -1), 'TOP'), style = [('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (0, 0), 0.0), ('LEFTPADDING', (0, 0), (0, 0), 0.0),
('BOTTOMPADDING', (0, 0), (-1, -1), 12.), ('BOTTOMPADDING', (0, 0), (-1, -1), 12.),
('FONTNAME', (0, 0), (-1, -1), 'Futura')] ('FONTNAME', (0, 0), (-1, -1), font_name)]
return Table(data=[row], style=style, colWidths=[1.0 * inch, 6.5 * inch]) return Table(data=[row], style=style, colWidths=[1.0 * inch, 6.5 * inch])

View File

@@ -148,7 +148,7 @@ def populate_columns(lines: List[ADRLine], columns, include_omitted, _page_size)
styles = list() styles = list()
columns_widths = list() columns_widths = list()
sorted_character_numbers = sorted(set([x.character_id for x in lines]), sorted_character_numbers: List[str] = sorted(set([x.character_id for x in lines]),
key=lambda x: str(x)) key=lambda x: str(x))
# construct column styles # construct column styles
@@ -164,18 +164,21 @@ def populate_columns(lines: List[ADRLine], columns, include_omitted, _page_size)
for n in sorted_character_numbers: for n in sorted_character_numbers:
char_records = [x for x in lines if x.character_id == n] char_records = [x for x in lines if x.character_id == n]
row_data = list() if len(char_records) > 0:
row_data2 = list() row_data = list()
for col in columns: row_data2 = list()
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) for col in columns:
data.append(row_data2) 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_row1 = list()
summary_row2 = list() summary_row2 = list()
@@ -202,16 +205,16 @@ def populate_columns(lines: List[ADRLine], columns, include_omitted, _page_size)
def output_report(lines: List[ADRLine], reel_list: List[str], include_omitted=False, def output_report(lines: List[ADRLine], reel_list: List[str], include_omitted=False,
page_size=portrait(letter)): page_size=portrait(letter), font_name='Helvetica'):
columns = build_columns(lines, include_omitted=include_omitted, reel_list=reel_list) columns = build_columns(lines, include_omitted=include_omitted, reel_list=reel_list)
data, style, columns_widths = populate_columns(lines, columns, include_omitted, page_size) data, style, columns_widths = populate_columns(lines, columns, include_omitted, page_size)
style.append(('FONTNAME', (0, 0), (-1, -1), "Futura")) style.append(('FONTNAME', (0, 0), (-1, -1), font_name))
style.append(('FONTSIZE', (0, 0), (-1, -1), 9.)) style.append(('FONTSIZE', (0, 0), (-1, -1), 9.))
style.append(('LINEBELOW', (0, 0), (-1, 0), 1.0, colors.black)) style.append(('LINEBELOW', (0, 0), (-1, 0), 1.0, colors.black))
# style.append(('LINEBELOW', (0, 1), (-1, -1), 0.25, colors.gray)) # style.append(('LINEBELOW', (0, 1), (-1, -1), 0.25, colors.gray))
pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc')) #pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
title = "%s Line Count" % lines[0].title title = "%s Line Count" % lines[0].title
filename = title + '.pdf' filename = title + '.pdf'
@@ -230,7 +233,7 @@ def output_report(lines: List[ADRLine], reel_list: List[str], include_omitted=Fa
story = [Spacer(height=0.5 * inch, width=1.), table] story = [Spacer(height=0.5 * inch, width=1.), table]
style = getSampleStyleSheet()['Normal'] style = getSampleStyleSheet()['Normal']
style.fontName = 'Futura' style.fontName = font_name
style.fontSize = 12. style.fontSize = 12.
style.spaceBefore = 16. style.spaceBefore = 16.
style.spaceAfter = 16. style.spaceAfter = 16.

View File

@@ -34,23 +34,26 @@ def build_aux_data_field(line: ADRLine):
elif line.adlib: elif line.adlib:
bg_color = 'purple' bg_color = 'purple'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " % (bg_color, fg_color, "ADLIB") tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font> " % (bg_color, fg_color, "ADLIB")
elif line.optional:
bg_color = 'green'
tag_field += "<font backColor=%s textColor=%s fontSize=11>%s</font>" % (bg_color, fg_color, "OPTIONAL")
entries.append(tag_field) entries.append(tag_field)
return "<br />".join(entries) return "<br />".join(entries)
def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat): def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat, font_name='Helvetica'):
story = list() story = list()
this_scene = None this_scene = None
scene_style = getSampleStyleSheet()['Normal'] scene_style = getSampleStyleSheet()['Normal']
scene_style.fontName = 'Futura' scene_style.fontName = font_name
scene_style.leftIndent = 0. scene_style.leftIndent = 0.
scene_style.leftPadding = 0. scene_style.leftPadding = 0.
scene_style.spaceAfter = 18. scene_style.spaceAfter = 18.
line_style = getSampleStyleSheet()['Normal'] line_style = getSampleStyleSheet()['Normal']
line_style.fontName = 'Futura' line_style.fontName = font_name
for line in lines: for line in lines:
table_style = [('VALIGN', (0, 0), (-1, -1), 'TOP'), table_style = [('VALIGN', (0, 0), (-1, -1), 'TOP'),

View File

@@ -11,11 +11,12 @@ from reportlab.platypus import Paragraph
from .__init__ import GRect from .__init__ import GRect
from ptulsconv.broadcast_timecode import TimecodeFormat from ptulsconv.broadcast_timecode import TimecodeFormat, footage_to_frame_count
from ptulsconv.docparser.adr_entity import ADRLine from ptulsconv.docparser.adr_entity import ADRLine
import datetime import datetime
font_name = 'Helvetica'
def draw_header_block(canvas, rect, record: ADRLine): def draw_header_block(canvas, rect, record: ADRLine):
rect.draw_text_cell(canvas, record.cue_number, "Helvetica", 44, vertical_align='m') rect.draw_text_cell(canvas, record.cue_number, "Helvetica", 44, vertical_align='m')
@@ -23,19 +24,19 @@ def draw_header_block(canvas, rect, record: ADRLine):
def draw_character_row(canvas, rect, record: ADRLine): def draw_character_row(canvas, rect, record: ADRLine):
label_frame, value_frame = rect.split_x(1.25 * inch) label_frame, value_frame = rect.split_x(1.25 * inch)
label_frame.draw_text_cell(canvas, "CHARACTER", "Futura", 10, force_baseline=9.) label_frame.draw_text_cell(canvas, "CHARACTER", font_name, 10, force_baseline=9.)
line = "%s / %s" % (record.character_id, record.character_name) line = "%s / %s" % (record.character_id, record.character_name)
if record.actor_name is not None: if record.actor_name is not None:
line = line + " / " + record.actor_name line = line + " / " + record.actor_name
value_frame.draw_text_cell(canvas, line, "Futura", 12, force_baseline=9.) value_frame.draw_text_cell(canvas, line, font_name, 12, force_baseline=9.)
rect.draw_border(canvas, ['min_y', 'max_y']) rect.draw_border(canvas, ['min_y', 'max_y'])
def draw_cue_number_block(canvas, rect, record: ADRLine): def draw_cue_number_block(canvas, rect, record: ADRLine):
(label_frame, number_frame,), aux_frame = rect.divide_y([0.20 * inch, 0.375 * inch], direction='d') (label_frame, number_frame,), aux_frame = rect.divide_y([0.20 * inch, 0.375 * inch], direction='d')
label_frame.draw_text_cell(canvas, "CUE NUMBER", "Futura", 10, label_frame.draw_text_cell(canvas, "CUE NUMBER", font_name, 10,
inset_y=5., vertical_align='t') inset_y=5., vertical_align='t')
number_frame.draw_text_cell(canvas, record.cue_number, "Futura", 14, number_frame.draw_text_cell(canvas, record.cue_number, font_name, 14,
inset_x=10., inset_y=2., draw_baseline=True) inset_x=10., inset_y=2., draw_baseline=True)
tags = {'tv': 'TV', tags = {'tv': 'TV',
@@ -49,7 +50,7 @@ def draw_cue_number_block(canvas, rect, record: ADRLine):
if getattr(record, key): if getattr(record, key):
tag_field = tag_field + tags[key] + " " tag_field = tag_field + tags[key] + " "
aux_frame.draw_text_cell(canvas, tag_field, "Futura", 10, aux_frame.draw_text_cell(canvas, tag_field, font_name, 10,
inset_x=10., inset_y=2., vertical_align='t') inset_x=10., inset_y=2., vertical_align='t')
rect.draw_border(canvas, 'max_x') rect.draw_border(canvas, 'max_x')
@@ -58,13 +59,13 @@ def draw_timecode_block(canvas, rect, record: ADRLine, tc_display_format: Timeco
(in_label_frame, in_frame, out_label_frame, out_frame), _ = rect.divide_y( (in_label_frame, in_frame, out_label_frame, out_frame), _ = rect.divide_y(
[0.20 * inch, 0.25 * inch, 0.20 * inch, 0.25 * inch], direction='d') [0.20 * inch, 0.25 * inch, 0.20 * inch, 0.25 * inch], direction='d')
in_label_frame.draw_text_cell(canvas, "IN", "Futura", 10, in_label_frame.draw_text_cell(canvas, "IN", font_name, 10,
vertical_align='t', inset_y=5., inset_x=5.) vertical_align='t', inset_y=5., inset_x=5.)
in_frame.draw_text_cell(canvas, tc_display_format.seconds_to_smpte(record.start), "Futura", 14, 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) inset_x=10., inset_y=2., draw_baseline=True)
out_label_frame.draw_text_cell(canvas, "OUT", "Futura", 10, out_label_frame.draw_text_cell(canvas, "OUT", font_name, 10,
vertical_align='t', inset_y=5., inset_x=5.) vertical_align='t', inset_y=5., inset_x=5.)
out_frame.draw_text_cell(canvas, tc_display_format.seconds_to_smpte(record.finish), "Futura", 14, 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) inset_x=10., inset_y=2., draw_baseline=True)
rect.draw_border(canvas, 'max_x') rect.draw_border(canvas, 'max_x')
@@ -75,16 +76,16 @@ def draw_reason_block(canvas, rect, record: ADRLine):
reason_label, reason_value = reason_cell.split_x(.75 * inch) reason_label, reason_value = reason_cell.split_x(.75 * inch)
notes_label, notes_value = notes_cell.split_x(.75 * inch) notes_label, notes_value = notes_cell.split_x(.75 * inch)
reason_label.draw_text_cell(canvas, "Reason:", "Futura", 12, reason_label.draw_text_cell(canvas, "Reason:", font_name, 12,
inset_x=5., inset_y=5., vertical_align='b') inset_x=5., inset_y=5., vertical_align='b')
reason_value.draw_text_cell(canvas, record.reason or "", "Futura", 12, reason_value.draw_text_cell(canvas, record.reason or "", font_name, 12,
inset_x=5., inset_y=5., draw_baseline=True, inset_x=5., inset_y=5., draw_baseline=True,
vertical_align='b') vertical_align='b')
notes_label.draw_text_cell(canvas, "Note:", "Futura", 12, notes_label.draw_text_cell(canvas, "Note:", font_name, 12,
inset_x=5., inset_y=5., vertical_align='t') inset_x=5., inset_y=5., vertical_align='t')
style = getSampleStyleSheet()['BodyText'] style = getSampleStyleSheet()['BodyText']
style.fontName = 'Futura' style.fontName = font_name
style.fontSize = 12 style.fontSize = 12
style.leading = 14 style.leading = 14
@@ -96,10 +97,10 @@ def draw_reason_block(canvas, rect, record: ADRLine):
def draw_prompt(canvas, rect, prompt=""): def draw_prompt(canvas, rect, prompt=""):
label, block = rect.split_y(0.20 * inch, direction='d') label, block = rect.split_y(0.20 * inch, direction='d')
label.draw_text_cell(canvas, "PROMPT", "Futura", 10, vertical_align='t', inset_y=5., inset_x=0.) label.draw_text_cell(canvas, "PROMPT", font_name, 10, vertical_align='t', inset_y=5., inset_x=0.)
style = getSampleStyleSheet()['BodyText'] style = getSampleStyleSheet()['BodyText']
style.fontName = 'Futura' style.fontName = font_name
style.fontSize = 14 style.fontSize = 14
style.leading = 24 style.leading = 24
@@ -116,10 +117,10 @@ def draw_prompt(canvas, rect, prompt=""):
def draw_notes(canvas, rect, note=""): def draw_notes(canvas, rect, note=""):
label, block = rect.split_y(0.20 * inch, direction='d') label, block = rect.split_y(0.20 * inch, direction='d')
label.draw_text_cell(canvas, "NOTES", "Futura", 10, vertical_align='t', inset_y=5., inset_x=0.) label.draw_text_cell(canvas, "NOTES", font_name, 10, vertical_align='t', inset_y=5., inset_x=0.)
style = getSampleStyleSheet()['BodyText'] style = getSampleStyleSheet()['BodyText']
style.fontName = 'Futura' style.fontName = font_name
style.fontSize = 14 style.fontSize = 14
style.leading = 24 style.leading = 24
@@ -175,12 +176,12 @@ def draw_aux_block(canvas, rect, recording_time_sec_this_line, recording_time_se
lines, last_line = content_rect.divide_y([12., 12., 24., 24., 24., 24.], direction='d') lines, last_line = content_rect.divide_y([12., 12., 24., 24., 24., 24.], direction='d')
lines[0].draw_text_cell(canvas, lines[0].draw_text_cell(canvas,
"Time for this line: %.1f mins" % (recording_time_sec_this_line / 60.), "Futura", 9.) "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.), "Futura", 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: ______________", "Futura", 9., vertical_align='b') lines[2].draw_text_cell(canvas, "Actual Start: ______________", font_name, 9., vertical_align='b')
lines[3].draw_text_cell(canvas, "Record Date: ______________", "Futura", 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: ______________", "Futura", 9., vertical_align='b') lines[4].draw_text_cell(canvas, "Engineer: ______________", font_name, 9., vertical_align='b')
lines[5].draw_text_cell(canvas, "Location: ______________", "Futura", 9., vertical_align='b') lines[5].draw_text_cell(canvas, "Location: ______________", font_name, 9., vertical_align='b')
def draw_footer(canvas, rect, record: ADRLine, report_date, line_no, total_lines): def draw_footer(canvas, rect, record: ADRLine, report_date, line_no, total_lines):
@@ -189,7 +190,7 @@ def draw_footer(canvas, rect, record: ADRLine, report_date, line_no, total_lines
spotting_name = [record.spot] if record.spot is not None else [] spotting_name = [record.spot] if record.spot is not None else []
pages_s = ["Line %i of %i" % (line_no, total_lines)] pages_s = ["Line %i of %i" % (line_no, total_lines)]
footer_s = " - ".join(report_date_s + spotting_name + pages_s) footer_s = " - ".join(report_date_s + spotting_name + pages_s)
rect.draw_text_cell(canvas, footer_s, font_name="Futura", font_size=10., inset_y=2.) rect.draw_text_cell(canvas, footer_s, font_name=font_name, font_size=10., inset_y=2.)
def create_report_for_character(records, report_date, tc_display_format: TimecodeFormat): def create_report_for_character(records, report_date, tc_display_format: TimecodeFormat):
@@ -200,7 +201,7 @@ def create_report_for_character(records, report_date, tc_display_format: Timecod
assert outfile is not None assert outfile is not None
assert outfile[-4:] == '.pdf', "Output file must have 'pdf' extension!" assert outfile[-4:] == '.pdf', "Output file must have 'pdf' extension!"
pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc')) #pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
page: GRect = GRect(0, 0, letter[0], letter[1]) page: GRect = GRect(0, 0, letter[0], letter[1])
page = page.inset(inch * 0.5) page = page.inset(inch * 0.5)

View File

@@ -16,15 +16,15 @@ from ..broadcast_timecode import TimecodeFormat
from ..docparser.adr_entity import ADRLine from ..docparser.adr_entity import ADRLine
def output_report(lines: List[ADRLine], tc_display_format: TimecodeFormat): def output_report(lines: List[ADRLine], tc_display_format: TimecodeFormat, font_name="Helvetica"):
character_numbers = set([n.character_id for n in lines]) character_numbers = set([n.character_id for n in lines])
pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc')) #pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc'))
for n in character_numbers: for n in character_numbers:
char_lines = [line for line in lines if not line.omitted and line.character_id == n] char_lines = [line for line in lines if not line.omitted and line.character_id == n]
character_name = char_lines[0].character_name character_name = char_lines[0].character_name
sorted(char_lines, key=lambda line: line.start) char_lines = sorted(char_lines, key=lambda line: line.start)
title = "%s (%s) %s ADR Script" % (char_lines[0].title, character_name, n) title = "%s (%s) %s ADR Script" % (char_lines[0].title, character_name, n)
filename = "%s_%s_%s_ADR Script.pdf" % (char_lines[0].title, n, character_name) filename = "%s_%s_%s_ADR Script.pdf" % (char_lines[0].title, n, character_name)
@@ -39,7 +39,7 @@ def output_report(lines: List[ADRLine], tc_display_format: TimecodeFormat):
story = [] story = []
prompt_style = getSampleStyleSheet()['Normal'] prompt_style = getSampleStyleSheet()['Normal']
prompt_style.fontName = 'Futura' prompt_style.fontName = font_name
prompt_style.fontSize = 18. prompt_style.fontSize = 18.
prompt_style.leading = 24. prompt_style.leading = 24.
@@ -47,7 +47,7 @@ def output_report(lines: List[ADRLine], tc_display_format: TimecodeFormat):
prompt_style.rightIndent = 1.5 * inch prompt_style.rightIndent = 1.5 * inch
number_style = getSampleStyleSheet()['Normal'] number_style = getSampleStyleSheet()['Normal']
number_style.fontName = 'Futura' number_style.fontName = font_name
number_style.fontSize = 14 number_style.fontSize = 14
number_style.leading = 24 number_style.leading = 24

View File

@@ -1,5 +1,15 @@
setuptools~=56.2.0 astroid==2.9.3
reportlab~=3.5.67 isort==5.10.1
ffmpeg~=1.4 lazy-object-proxy==1.7.1
parsimonious~=0.8.1 mccabe==0.6.1
tqdm~=4.60.0 parsimonious==0.9.0
Pillow==9.1.1
platformdirs==2.4.1
pylint==2.12.2
regex==2022.6.2
reportlab==3.6.10
six==1.16.0
toml==0.10.2
tqdm==4.64.0
typing_extensions==4.0.1
wrapt==1.13.3

View File

@@ -25,7 +25,8 @@ setup(name='ptulsconv',
'Topic :: Multimedia :: Sound/Audio', 'Topic :: Multimedia :: Sound/Audio',
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Topic :: Text Processing :: Filters"], "Topic :: Text Processing :: Filters"],
packages=['ptulsconv'], packages=['ptulsconv'],

4
test-coverage.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
coverage run -m pytest . ; coverage-lcov

View File

@@ -4,7 +4,7 @@ import os.path
class TestRobinHood1(unittest.TestCase): class TestRobinHood1(unittest.TestCase):
path = os.path.dirname(__file__) + '/export_cases/Robin Hood Spotting.txt' path = os.path.dirname(__file__) + '/../export_cases/Robin Hood Spotting.txt'
def test_header_export(self): def test_header_export(self):

View File

@@ -4,7 +4,7 @@ import os.path
class TestRobinHood5(unittest.TestCase): class TestRobinHood5(unittest.TestCase):
path = os.path.dirname(__file__) + '/export_cases/Robin Hood Spotting5.txt' path = os.path.dirname(__file__) + '/../export_cases/Robin Hood Spotting5.txt'
def test_skipped_segments(self): def test_skipped_segments(self):
session = parse_document(self.path) session = parse_document(self.path)

View File

@@ -4,7 +4,7 @@ import os.path
class TestRobinHood6(unittest.TestCase): class TestRobinHood6(unittest.TestCase):
path = os.path.dirname(__file__) + '/export_cases/Robin Hood Spotting6.txt' path = os.path.dirname(__file__) + '/../export_cases/Robin Hood Spotting6.txt'
def test_a_track(self): def test_a_track(self):
session = parse_document(self.path) session = parse_document(self.path)

View File

@@ -4,7 +4,7 @@ import os.path
class TestRobinHoodDF(unittest.TestCase): class TestRobinHoodDF(unittest.TestCase):
path = os.path.dirname(__file__) + '/export_cases/Robin Hood SpottingDF.txt' path = os.path.dirname(__file__) + '/../export_cases/Robin Hood SpottingDF.txt'
def test_header_export_df(self): def test_header_export_df(self):
session = parse_document(self.path) session = parse_document(self.path)

View File

@@ -0,0 +1,34 @@
import unittest
import tempfile
import os.path
import os
import glob
from ptulsconv import commands
class TestBroadcastTimecode(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 = [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
finally:
tempdir.cleanup()
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,15 @@
import unittest
from ptulsconv import footage
class TestFootage(unittest.TestCase):
def test_basic_footage(self):
r1 = "90+0"
f1 = footage.footage_to_seconds(r1)
self.assertEqual(float(f1 or 0), 60.0)
def test_feet_and_frames(self):
r1 = "1+8"
f1 = footage.footage_to_seconds(r1)
self.assertEqual(float(f1 or 0), 1.0)

View File

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

View File

@@ -4,7 +4,7 @@ import os.path
class TaggingIntegratedTests(unittest.TestCase): class TaggingIntegratedTests(unittest.TestCase):
path = os.path.dirname(__file__) + '/export_cases/Tag Tests/Tag Tests.txt' path = os.path.dirname(__file__) + '/../export_cases/Tag Tests/Tag Tests.txt'
def test_event_list(self): def test_event_list(self):
with open(self.path, 'r') as f: with open(self.path, 'r') as f: