From b46fc85b1642dc9d3c1b9d2eca66dab267370545 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Thu, 3 Jun 2021 10:19:33 -0700 Subject: [PATCH] Refactoring reports to use docparser --- .idea/dictionaries/jamie.xml | 1 + ptulsconv/__main__.py | 21 +---- ptulsconv/broadcast_timecode.py | 37 +++++--- ptulsconv/commands.py | 139 ++++++++++++++-------------- ptulsconv/docparser/adr_entity.py | 7 +- ptulsconv/docparser/doc_entity.py | 36 +++---- ptulsconv/docparser/tag_compiler.py | 26 ++++-- ptulsconv/pdf/__init__.py | 21 +++-- ptulsconv/pdf/summary_log.py | 108 ++++++++++----------- tests/test_broadcast_timecode.py | 26 +++--- tests/test_tag_compiler.py | 3 + 11 files changed, 219 insertions(+), 206 deletions(-) diff --git a/.idea/dictionaries/jamie.xml b/.idea/dictionaries/jamie.xml index c97c337..b8761aa 100644 --- a/.idea/dictionaries/jamie.xml +++ b/.idea/dictionaries/jamie.xml @@ -6,6 +6,7 @@ futura ptulsconv retval + smpte timecode timespan diff --git a/ptulsconv/__main__.py b/ptulsconv/__main__.py index 1105aa0..b141b04 100644 --- a/ptulsconv/__main__.py +++ b/ptulsconv/__main__.py @@ -1,4 +1,4 @@ -from ptulsconv.commands import convert, dump_field_map, raw_output +from ptulsconv.commands import convert, dump_field_map from ptulsconv import __name__, __version__, __author__ from optparse import OptionParser, OptionGroup from .xml.common import dump_xform_options @@ -60,17 +60,13 @@ def main(): (options, args) = parser.parse_args(sys.argv) - if options.output_format == 'raw': - raw_output(args[1]) - exit(0) - print_banner_style("%s %s (c) 2020 %s. All rights reserved." % (__name__, __version__, __author__)) print_section_header_style("Startup") print_status_style("This run started %s" % (datetime.datetime.now().isoformat())) if options.show_tags: - dump_field_map('ADR') + dump_field_map() sys.exit(0) if options.show_transforms: @@ -84,16 +80,6 @@ def main(): print_status_style("Input file is %s" % (args[1])) - # if options.in_time: - # print_status_style("Start at time %s" % (options.in_time)) - # else: - # print_status_style("No start time given.") - # - # if options.out_time: - # print_status_style("End at time %s." % (options.out_time)) - # else: - # print_status_style("No end time given.") - if options.include_muted: print_status_style("Muted regions are included.") else: @@ -105,11 +91,8 @@ def main(): output_format = 'fmpxml' convert(input_file=args[1], output_format=output_format, - #start=options.in_time, - #end=options.out_time, include_muted=options.include_muted, xsl=options.xslt, - #select_reel=options.select_reel, progress=False, output=sys.stdout, log_output=sys.stderr, warnings=options.warnings) except FileNotFoundError as e: diff --git a/ptulsconv/broadcast_timecode.py b/ptulsconv/broadcast_timecode.py index 4c2ce1a..6397e6e 100644 --- a/ptulsconv/broadcast_timecode.py +++ b/ptulsconv/broadcast_timecode.py @@ -1,6 +1,22 @@ from fractions import Fraction import re import math +from collections import namedtuple + + +class TimecodeFormat(namedtuple("_TimecodeFormat", "frame_duration logical_fps drop_frame")): + + def smpte_to_seconds(self, smpte: str) -> Fraction: + frame_count = smpte_to_frame_count(smpte, self.logical_fps, drop_frame_hint=self.drop_frame) + return frame_count * self.frame_duration + + def seconds_to_smpte(self, seconds: Fraction) -> str: + frame_count = int(seconds / self.frame_duration) + return frame_count_to_smpte(frame_count, self.logical_fps, self.drop_frame) + + + + def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int, drop_frame_hint=False) -> int: @@ -39,14 +55,11 @@ def smpte_to_frame_count(smpte_rep_string: str, frames_per_logical_second: int, dropped_frames = frames_dropped_per_inst * inst_count frames = raw_frames - dropped_frames - # if include_fractional: - # return frames, frac - # else: return frames def frame_count_to_smpte(frame_count: int, frames_per_logical_second: int, drop_frame: bool = False, - fractional_frame: float = None): + fractional_frame: float = None) -> str: assert frames_per_logical_second in [24, 25, 30, 48, 50, 60] assert fractional_frame is None or fractional_frame < 1.0 @@ -72,24 +85,18 @@ 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) -def footage_to_frame_count(footage_string, include_fractional=False): +def footage_to_frame_count(footage_string): m = re.search("(\d+)\+(\d+)(\.\d+)?", footage_string) feet, frm, frac = m.groups() feet, frm, frac = int(feet), int(frm), float(frac or 0.0) frames = feet * 16 + frm - if include_fractional: - return frames, frac - else: - return frames + return frames -def frame_count_to_footage(frame_count, fractional_frames=None): - assert fractional_frames is None or fractional_frames < 1.0 +def frame_count_to_footage(frame_count): feet, frm = divmod(frame_count, 16) + return "%i+%02i" % (feet, frm) + - if fractional_frames is None: - return "%i+%02i" % (feet, frm) - else: - return "%i+%02i%s" % (feet, frm, ("%.3f" % fractional_frames)[1:]) diff --git a/ptulsconv/commands.py b/ptulsconv/commands.py index eb291ad..3e6bd65 100644 --- a/ptulsconv/commands.py +++ b/ptulsconv/commands.py @@ -11,6 +11,8 @@ from .validations import * from ptulsconv.docparser import parse_document from ptulsconv.docparser.tag_compiler import TagCompiler +from ptulsconv.broadcast_timecode import TimecodeFormat +from fractions import Fraction from ptulsconv.pdf.supervisor_1pg import output_report as output_supervisor_1pg from ptulsconv.pdf.line_count import output_report as output_line_count @@ -18,10 +20,18 @@ from ptulsconv.pdf.talent_sides import output_report as output_talent_sides from ptulsconv.pdf.summary_log import output_report as output_summary from json import JSONEncoder + + class MyEncoder(JSONEncoder): - def default(self, o): + force_denominator: Optional[int] + + def default(self, o): + if isinstance(o, Fraction): + return dict(numerator=o.numerator, denominator=o.denominator) + else: return o.__dict__ + def dump_csv(events, output=sys.stdout): keys = set() for e in events: @@ -69,6 +79,7 @@ def output_adr_csv(lines): with open(outfile_name, mode='w', newline='') as outfile: dump_keyed_csv(these_lines, adr_keys, outfile) + def output_avid_markers(lines): reels = set([ln['Reel'] for ln in lines if 'Reel' in ln.keys()]) @@ -76,44 +87,43 @@ def output_avid_markers(lines): pass -def create_adr_reports(parsed): - lines = [e for e in parsed['events'] if 'ADR' in e.keys()] +def create_adr_reports(lines: List[ADRLine], tc_display_format: TimecodeFormat): print_section_header_style("Creating PDF Reports") print_status_style("Creating ADR Report") - output_summary(lines) + output_summary(lines, tc_display_format=tc_display_format) - print_status_style("Creating Line Count") - output_line_count(lines) - - print_status_style("Creating Supervisor Logs directory and reports") - os.makedirs("Supervisor Logs", exist_ok=True) - os.chdir("Supervisor Logs") - output_supervisor_1pg(lines) - os.chdir("..") - - print_status_style("Creating Director's Logs director and reports") - os.makedirs("Director Logs", exist_ok=True) - os.chdir("Director Logs") - output_summary(lines, by_character=True) - os.chdir("..") - - print_status_style("Creating CSV outputs") - os.makedirs("CSV", exist_ok=True) - os.chdir("CSV") - output_adr_csv(lines) - os.chdir("..") - - print_status_style("Creating Avid Marker XML files") - os.makedirs("Avid Markers", exist_ok=True) - os.chdir("Avid Markers") - output_avid_markers(lines) - os.chdir("..") - - print_status_style("Creating Scripts directory and reports") - os.makedirs("Talent Scripts", exist_ok=True) - os.chdir("Talent Scripts") - output_talent_sides(lines) + # print_status_style("Creating Line Count") + # output_line_count(lines) + # + # print_status_style("Creating Supervisor Logs directory and reports") + # os.makedirs("Supervisor Logs", exist_ok=True) + # os.chdir("Supervisor Logs") + # output_supervisor_1pg(lines) + # os.chdir("..") + # + # print_status_style("Creating Director's Logs director and reports") + # os.makedirs("Director Logs", exist_ok=True) + # os.chdir("Director Logs") + # output_summary(lines, tc_display_format=tc_display_format, by_character=True) + # os.chdir("..") + # + # print_status_style("Creating CSV outputs") + # os.makedirs("CSV", exist_ok=True) + # os.chdir("CSV") + # output_adr_csv(lines) + # os.chdir("..") + # + # print_status_style("Creating Avid Marker XML files") + # os.makedirs("Avid Markers", exist_ok=True) + # os.chdir("Avid Markers") + # output_avid_markers(lines) + # os.chdir("..") + # + # print_status_style("Creating Scripts directory and reports") + # os.makedirs("Talent Scripts", exist_ok=True) + # os.chdir("Talent Scripts") + # output_talent_sides(lines) def parse_text_export(file): @@ -127,51 +137,44 @@ def parse_text_export(file): return parsed -def raw_output(input_file, output=sys.stdout): - from .docparser.doc_parser_visitor import DocParserVisitor - from json import JSONEncoder - - class DescriptorJSONEncoder(JSONEncoder): - def default(self, obj): - return obj.__dict__ - - with open(input_file, 'r') as file: - ast = ptulsconv.protools_text_export_grammar.parse(file.read()) - visitor = DocParserVisitor() - parsed = visitor.visit(ast) - json.dump(parsed, output, cls=DescriptorJSONEncoder) - - def convert(input_file, output_format='fmpxml', progress=False, include_muted=False, xsl=None, output=sys.stdout, log_output=sys.stderr, warnings=True): session = parse_document(input_file) - compiler = TagCompiler() - compiler.session = session - compiled_events = compiler.compile_events() + session_tc_format = session.header.timecode_format - lines = list(map(ADRLine.from_event, compiled_events)) + if output_format == 'raw': + output.write(MyEncoder().encode(session)) - if warnings: - for warning in chain(validate_unique_field(lines, field='cue_number'), - validate_non_empty_field(lines, field='cue_number'), - validate_non_empty_field(lines, field='character_id'), - validate_non_empty_field(lines, field='title'), - validate_dependent_value(lines, key_field='character_id', - dependent_field='character_name'), - validate_dependent_value(lines, key_field='character_id', - dependent_field='actor_name')): - print_warning(warning.report_message()) + else: - if output_format == 'json': - print(MyEncoder().encode(lines)) + compiler = TagCompiler() + compiler.session = session + compiled_events = list(compiler.compile_events()) + if output_format == 'json': + output.write(MyEncoder().encode(compiled_events)) + + else: + lines = list(map(ADRLine.from_event, compiled_events)) + + if warnings: + for warning in chain(validate_unique_field(lines, field='cue_number'), + validate_non_empty_field(lines, field='cue_number'), + validate_non_empty_field(lines, field='character_id'), + validate_non_empty_field(lines, field='title'), + validate_dependent_value(lines, key_field='character_id', + dependent_field='character_name'), + validate_dependent_value(lines, key_field='character_id', + dependent_field='actor_name')): + print_warning(warning.report_message()) + + if output_format == 'adr': + create_adr_reports(lines, tc_display_format=session_tc_format) # elif output_format == 'csv': # dump_csv(parsed['events']) # - # elif output_format == 'adr': - # create_adr_reports(parsed) # elif output_format == 'fmpxml': # if xsl is None: diff --git a/ptulsconv/docparser/adr_entity.py b/ptulsconv/docparser/adr_entity.py index 43838dc..2d41c7d 100644 --- a/ptulsconv/docparser/adr_entity.py +++ b/ptulsconv/docparser/adr_entity.py @@ -1,6 +1,7 @@ from ptulsconv.docparser.tag_compiler import Event from typing import Optional from dataclasses import dataclass +from fractions import Fraction from ptulsconv.docparser.tag_mapping import TagMapping @@ -13,8 +14,8 @@ class ADRLine: scene: Optional[str] version: Optional[str] reel: Optional[str] - start: Optional[str] - finish: Optional[str] + start: Optional[Fraction] + finish: Optional[Fraction] priority: Optional[int] cue_number: Optional[str] character_id: Optional[str] @@ -101,6 +102,8 @@ class ADRLine: new = cls() TagMapping.apply_rules(cls.tag_mapping, event.tags, event.clip_name, event.track_name, event.session_name, new) + new.start = event.start + new.finish = event.finish return new diff --git a/ptulsconv/docparser/doc_entity.py b/ptulsconv/docparser/doc_entity.py index 7451c1d..8fd1f76 100644 --- a/ptulsconv/docparser/doc_entity.py +++ b/ptulsconv/docparser/doc_entity.py @@ -1,5 +1,5 @@ from fractions import Fraction -from ptulsconv.broadcast_timecode import smpte_to_frame_count +from ptulsconv.broadcast_timecode import TimecodeFormat from typing import Tuple, List, Iterator @@ -30,7 +30,7 @@ class SessionDescriptor: yield track, clip def track_clips_timed(self) -> Iterator[Tuple["TrackDescriptor", "TrackClipDescriptor", - Fraction, Fraction, Fraction]]: + Fraction, Fraction, Fraction]]: """ :return: A Generator that yields track, clip, start time, finish time, and timestamp """ @@ -48,7 +48,7 @@ class HeaderDescriptor: sample_rate: float bit_depth: int start_timecode: str - timecode_format: str + timecode_fps: str timecode_drop_frame: bool count_audio_tracks: int count_clips: int @@ -59,18 +59,20 @@ class HeaderDescriptor: self.sample_rate = kwargs['sample_rate'] self.bit_depth = kwargs['bit_depth'] self.start_timecode = kwargs['start_timecode'] - self.timecode_format = kwargs['timecode_format'] + self.timecode_fps = kwargs['timecode_format'] self.timecode_drop_frame = kwargs['timecode_drop_frame'] self.count_audio_tracks = kwargs['count_audio_tracks'] self.count_clips = kwargs['count_clips'] self.count_files = kwargs['count_files'] - def convert_timecode(self, tc_string: str) -> Fraction: - frame_count = smpte_to_frame_count(tc_string, - self.logical_fps, - self.timecode_drop_frame) + @property + def timecode_format(self): + return TimecodeFormat(frame_duration=self.frame_duration, + logical_fps=self.logical_fps, + drop_frame=self.timecode_drop_frame) - return self.frame_duration * frame_count + def convert_timecode(self, tc_string: str) -> Fraction: + return self.timecode_format.smpte_to_seconds(tc_string) @property def start_time(self) -> Fraction: @@ -91,16 +93,16 @@ class HeaderDescriptor: @property def _get_tc_format_params(self) -> Tuple[int, Fraction]: frame_rates = {"23.976": (24, Fraction(1001, 24_000)), - "24": (24, Fraction(1, 24)), - "25": (25, Fraction(1, 25)), - "29.97": (30, Fraction(1001, 30_000)), - "30": (30, Fraction(1, 30)), - "59.94": (60, Fraction(1001, 60_000)), - "60": (60, Fraction(1, 60)) + "24": (24, Fraction(1, 24)), + "25": (25, Fraction(1, 25)), + "29.97": (30, Fraction(1001, 30_000)), + "30": (30, Fraction(1, 30)), + "59.94": (60, Fraction(1001, 60_000)), + "60": (60, Fraction(1, 60)) } - if self.timecode_format in frame_rates.keys(): - return frame_rates[self.timecode_format] + if self.timecode_fps in frame_rates.keys(): + return frame_rates[self.timecode_fps] else: raise ValueError("Unrecognized TC rate (%s)" % self.timecode_format) diff --git a/ptulsconv/docparser/tag_compiler.py b/ptulsconv/docparser/tag_compiler.py index 9bd60cd..4f285c9 100644 --- a/ptulsconv/docparser/tag_compiler.py +++ b/ptulsconv/docparser/tag_compiler.py @@ -1,16 +1,28 @@ from collections import namedtuple from fractions import Fraction -from typing import Iterator, Tuple, Callable, Generator +from typing import Iterator, Tuple, Callable, Generator, Dict import ptulsconv.docparser.doc_entity as doc_entity from .tagged_string_parser_visitor import parse_tags, TagPreModes +from dataclasses import dataclass -class Event(namedtuple('Event', 'clip_name track_name session_name tags start finish')): - pass + +@dataclass +class Event: + clip_name: str + track_name: str + session_name: str + tags: Dict[str, str] + start: Fraction + finish: Fraction class TagCompiler: + + Intermediate = namedtuple('Intermediate', 'track_content track_tags track_comment_tags ' + 'clip_content clip_tags clip_tag_mode start finish') + session: doc_entity.SessionDescriptor def compile_events(self) -> Iterator[Event]: @@ -19,11 +31,12 @@ class TagCompiler: step2 = self.collect_time_spans(step1) step3 = self.apply_tags(step2) for datum in step3: - yield Event(*datum) + yield Event(clip_name=datum[0], track_name=datum[1], session_name=datum[2], + tags=datum[3], start=datum[4], finish=datum[5]) def _marker_tags(self, at): retval = dict() - 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]): retval.update(parse_tags(marker.comments).tag_dict) retval.update(parse_tags(marker.name).tag_dict) @@ -44,9 +57,6 @@ class TagCompiler: effective_tags.update(clip_tags) return effective_tags - Intermediate = namedtuple('Intermediate', 'track_content track_tags track_comment_tags ' - 'clip_content clip_tags clip_tag_mode start finish') - def parse_data(self) -> Iterator[Intermediate]: for track, clip, start, finish, _ in self.session.track_clips_timed(): diff --git a/ptulsconv/pdf/__init__.py b/ptulsconv/pdf/__init__.py index 4653a7a..1cdb28b 100644 --- a/ptulsconv/pdf/__init__.py +++ b/ptulsconv/pdf/__init__.py @@ -8,6 +8,8 @@ from reportlab.platypus.frames import Frame from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont +from ptulsconv.docparser.adr_entity import ADRLine + # This is from https://code.activestate.com/recipes/576832/ for # generating page count messages @@ -50,7 +52,7 @@ class ADRDocTemplate(BaseDocTemplate): BaseDocTemplate.build(self, flowables, filename, canvasmaker) -def make_doc_template(page_size, filename, document_title, record, document_header="", left_margin=0.5 * inch): +def make_doc_template(page_size, filename, document_title, record: ADRLine, document_header="", left_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 = page_box.split_x(left_margin, direction='l') @@ -70,7 +72,7 @@ def make_doc_template(page_size, filename, document_title, record, document_head pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc')) doc = ADRDocTemplate(filename, title=document_title, - author=record.get('Supervisor', ""), + author=record.supervisor, pagesize=page_size, leftMargin=left_margin, rightMargin=right_margin, topMargin=top_margin, bottomMargin=bottom_margin) @@ -91,11 +93,11 @@ def time_format(mins, zero_str=""): return "%i:%02i" % (hh, mm) -def draw_header_footer(a_canvas: ReportCanvas, title_box, doc_title_box, footer_box, record, doc_title=""): +def draw_header_footer(a_canvas: ReportCanvas, title_box, doc_title_box, footer_box, record: ADRLine, doc_title=""): (supervisor, client,), title = title_box.divide_y([16., 16., ]) - title.draw_text_cell(a_canvas, record['Title'], "Futura", 18, inset_y=2., inset_x=5.) - client.draw_text_cell(a_canvas, record.get('Client', ''), "Futura", 11, inset_y=2., inset_x=5.) + title.draw_text_cell(a_canvas, record.title, "Futura", 18, inset_y=2., inset_x=5.) + client.draw_text_cell(a_canvas, record.client, "Futura", 11, inset_y=2., inset_x=5.) a_canvas.saveState() a_canvas.setLineWidth(0.5) @@ -114,12 +116,12 @@ def draw_header_footer(a_canvas: ReportCanvas, title_box, doc_title_box, footer_ doc_title_cell.draw_text_cell(a_canvas, doc_title, 'Futura', 14., inset_y=2.) - if 'Spot' in record.keys(): - spotting_version_cell.draw_text_cell(a_canvas, record['Spot'], 'Futura', 12., inset_y=2.) + if record.spot is not None: + spotting_version_cell.draw_text_cell(a_canvas, record.spot, 'Futura', 12., inset_y=2.) a_canvas.setFont('Futura', 11.) a_canvas.drawCentredString(footer_box.min_x + footer_box.width / 2., footer_box.min_y, - record.get('Supervisor', 'Supervisor: ________________')) + record.supervisor or "") class GRect: @@ -254,6 +256,9 @@ class GRect: def draw_text_cell(self, a_canvas, text, font_name, font_size, vertical_align='t', force_baseline=None, inset_x=0., inset_y=0., draw_baseline=False): + if text is None: + return + a_canvas.saveState() inset_rect = self.inset_xy(inset_x, inset_y) diff --git a/ptulsconv/pdf/summary_log.py b/ptulsconv/pdf/summary_log.py index 9fd0064..6a01f2e 100644 --- a/ptulsconv/pdf/summary_log.py +++ b/ptulsconv/pdf/summary_log.py @@ -6,40 +6,41 @@ from reportlab.lib.pagesizes import letter, portrait from reportlab.platypus import Paragraph, Spacer, KeepTogether, Table from reportlab.lib.styles import getSampleStyleSheet -from reportlab.lib import colors + +from typing import List +from ptulsconv.docparser.adr_entity import ADRLine +from ptulsconv.broadcast_timecode import TimecodeFormat -def build_aux_data_field(line): +def build_aux_data_field(line: ADRLine): entries = list() - if 'Reason' in line.keys(): - entries.append("Reason: " + line["Reason"]) - if 'Note' in line.keys(): - entries.append("Note: " + line["Note"]) - if 'Requested by' in line.keys(): - entries.append("Requested by: " + line["Requested by"]) - if 'Shot' in line.keys(): - entries.append("Shot: " + line["Shot"]) + if line.reason is not None: + entries.append("Reason: " + line.reason) + if line.note is not None: + entries.append("Note: " + line.note) + if line.requested_by is not None: + entries.append("Requested by: " + line.requested_by) + if line.shot is not None: + entries.append("Shot: " + line.shot) + fg_color = 'white' tag_field = "" - for tag in line.keys(): - if line[tag] == tag and tag != 'ADR': - fcolor = 'white' - bcolor = 'black' - if tag == 'ADLIB' or tag == 'TBW': - bcolor = 'darkmagenta' - elif tag == 'EFF': - bcolor = 'red' - elif tag == 'TV': - bcolor = 'blue' - - tag_field += "%s " % (bcolor, fcolor, tag) + if line.effort: + bg_color = 'red' + tag_field += "%s " % (bg_color, fg_color, "EFF") + elif line.tv: + bg_color = 'blue' + tag_field += "%s " % (bg_color, fg_color, "TV") + elif line.adlib: + bg_color = 'purple' + tag_field += "%s " % (bg_color, fg_color, "ADLIB") entries.append(tag_field) return "
".join(entries) -def build_story(lines): +def build_story(lines: List[ADRLine], tc_rate: TimecodeFormat): story = list() this_scene = None @@ -56,20 +57,20 @@ def build_story(lines): ('LEFTPADDING', (0, 0), (0, 0), 0.0), ('BOTTOMPADDING', (0, 0), (-1, -1), 24.)] - cue_number_field = "%s
%s" % (line['Cue Number'], line['Character Name']) + cue_number_field = "%s
%s" % (line.cue_number, line.character_name) - time_data = time_format(line.get('Time Budget Mins', 0.)) + time_data = time_format(line.time_budget_mins) - if 'Priority' in line.keys(): - time_data = time_data + "
" + "P: " + int(line['Priority']) + if line.priority is not None: + time_data = time_data + "
" + "P: " + line.priority aux_data_field = build_aux_data_field(line) - tc_data = build_tc_data(line) + tc_data = build_tc_data(line, tc_rate) line_table_data = [[Paragraph(cue_number_field, line_style), Paragraph(tc_data, line_style), - Paragraph(line['Line'], line_style), + Paragraph(line.prompt, line_style), Paragraph(time_data, line_style), Paragraph(aux_data_field, line_style) ]] @@ -78,8 +79,8 @@ def build_story(lines): colWidths=[inch * 0.75, inch, inch * 3., 0.5 * inch, inch * 2.], style=table_style) - if line.get('Scene', "[No Scene]") != this_scene: - this_scene = line.get('Scene', "[No Scene]") + if (line.scene or "[No Scene]") != this_scene: + this_scene = line.scene or "[No Scene]" story.append(KeepTogether([ Spacer(1., 0.25 * inch), Paragraph("" + this_scene + "", scene_style), @@ -91,52 +92,51 @@ def build_story(lines): return story -def build_tc_data(line): - tc_data = line['PT.Clip.Start'] + "
" + line['PT.Clip.Finish'] +def build_tc_data(line: ADRLine, tc_format: TimecodeFormat): + tc_data = tc_format.seconds_to_smpte(line.start) + "
" + \ + tc_format.seconds_to_smpte(line.finish) third_line = [] - if 'Reel' in line.keys(): - if line['Reel'][0:1] == 'R': - third_line.append("%s" % (line['Reel'])) + if line.reel is not None: + if line.reel[0:1] == 'R': + third_line.append("%s" % line.reel) else: - third_line.append("Reel %s" % (line['Reel'])) - if 'Version' in line.keys(): - third_line.append("(%s)" % line['Version']) + third_line.append("Reel %s" % line.reel) + if line.version is not None: + third_line.append("(%s)" % line.version) if len(third_line) > 0: tc_data = tc_data + "
" + " ".join(third_line) return tc_data -def generate_report(page_size, lines, character_number=None, include_done=True, +def generate_report(page_size, lines: List[ADRLine], tc_rate: TimecodeFormat, character_number=None, include_omitted=True): if character_number is not None: - lines = [r for r in lines if r['Character Number'] == character_number] - title = "%s ADR Report (%s)" % (lines[0]['Title'], lines[0]['Character Name']) - document_header = "%s ADR Report" % (lines[0]['Character Name']) + lines = [r for r in lines if r.character_id == character_number] + title = "%s ADR Report (%s)" % (lines[0].title, lines[0].character_name) + document_header = "%s ADR Report" % lines[0].character_name else: - title = "%s ADR Report" % (lines[0]['Title']) + title = "%s ADR Report" % lines[0].title document_header = 'ADR Report' - if not include_done: - lines = [line for line in lines if 'Done' not in line.keys()] - if not include_omitted: - lines = [line for line in lines if 'Omitted' not in line.keys()] + lines = [line for line in lines if not line.omitted] - lines = sorted(lines, key=lambda line: line['PT.Clip.Start_Seconds']) + lines = sorted(lines, key=lambda line: line.start) filename = title + ".pdf" doc = make_doc_template(page_size=page_size, filename=filename, document_title=title, record=lines[0], document_header=document_header, left_margin=0.75 * inch) - story = build_story(lines) + story = build_story(lines, tc_rate) doc.build(story) -def output_report(lines, page_size=portrait(letter), by_character=False): +def output_report(lines: List[ADRLine], tc_display_format: TimecodeFormat, + page_size=portrait(letter), by_character=False): if by_character: - character_numbers = set((r['Character Number'] for r in lines)) + character_numbers = set((r.character_id for r in lines)) for n in character_numbers: - generate_report(page_size, lines, n) + generate_report(page_size, lines, tc_display_format, n) else: - generate_report(page_size, lines) + generate_report(page_size, lines, tc_display_format) diff --git a/tests/test_broadcast_timecode.py b/tests/test_broadcast_timecode.py index 4f416b9..378dbbc 100644 --- a/tests/test_broadcast_timecode.py +++ b/tests/test_broadcast_timecode.py @@ -1,9 +1,9 @@ import unittest from ptulsconv import broadcast_timecode - +from fractions import Fraction class TestBroadcastTimecode(unittest.TestCase): - def test_basic_to_framecount(self): + def test_basic_to_frame_count(self): r1 = "01:00:00:00" f1 = broadcast_timecode.smpte_to_frame_count(r1, 24, False) self.assertEqual(f1, 86_400) @@ -32,13 +32,7 @@ class TestBroadcastTimecode(unittest.TestCase): s1 = broadcast_timecode.frame_count_to_smpte(c1, 30, drop_frame=True) self.assertEqual(s1, "01:00:03;18") - def test_fractional_to_string(self): - c1 = 99 - f1 = .145 - s1 = broadcast_timecode.frame_count_to_smpte(c1, 25, drop_frame=False, fractional_frame=f1) - self.assertEqual(s1, "00:00:03:24.145") - - def test_drop_frame_to_framecount(self): + def test_drop_frame_to_frame_count(self): r1 = "01:00:00;00" z1 = broadcast_timecode.smpte_to_frame_count(r1, 30, drop_frame_hint=True) self.assertEqual(z1, 107_892) @@ -55,13 +49,13 @@ class TestBroadcastTimecode(unittest.TestCase): f3 = broadcast_timecode.smpte_to_frame_count(r3, 30, True) self.assertEqual(f3, 1799) - def test_footage_to_framecount(self): + def test_footage_to_frame_count(self): s1 = "194+11" f1 = broadcast_timecode.footage_to_frame_count(s1) self.assertEqual(f1, 3115) s3 = "0+0.1" - f3 = broadcast_timecode.footage_to_frame_count(s3, include_fractional=False) + f3 = broadcast_timecode.footage_to_frame_count(s3) self.assertEqual(f3, 0) def test_frame_count_to_footage(self): @@ -69,10 +63,12 @@ class TestBroadcastTimecode(unittest.TestCase): s1 = broadcast_timecode.frame_count_to_footage(c1) self.assertEqual(s1, "1+03") - c2 = 24 - f2 = .1 - s2 = broadcast_timecode.frame_count_to_footage(c2, fractional_frames=f2) - self.assertEqual(s2, "1+08.100") + def test_seconds_to_smpte(self): + secs = Fraction(25, 24) + frame_duration = Fraction(1, 24) + s1 = broadcast_timecode.seconds_to_smpte(secs, frame_duration, 24, False) + self.assertEqual(s1, "00:00:01:01") + if __name__ == '__main__': unittest.main() diff --git a/tests/test_tag_compiler.py b/tests/test_tag_compiler.py index eae7ae4..03a210f 100644 --- a/tests/test_tag_compiler.py +++ b/tests/test_tag_compiler.py @@ -6,6 +6,9 @@ from fractions import Fraction class TestTagCompiler(unittest.TestCase): + + #TODO Test marker comment application + def test_one_track(self): c = ptulsconv.docparser.tag_compiler.TagCompiler()