def parse_shift_string(shift_string): try: if ':' in shift_string: negator = 1 if shift_string.startswith('-'): negator = -1 shift_string = shift_string[1:] parts = list(map(float, shift_string.split(':'))) if len(parts) > 3: raise PrassError( "Invalid shift value: '{0}'".format(shift_string)) shift_seconds = sum( part * multiplier for part, multiplier in zip(reversed(parts), (1.0, 60.0, 3600.0))) return shift_seconds * 1000 * negator # convert to ms else: if shift_string.endswith("ms"): return float(shift_string[:-2]) elif shift_string.endswith("s"): return float(shift_string[:-1]) * 1000 else: return float(shift_string) * 1000 except ValueError: raise PrassError("Invalid shift value: '{0}'".format(shift_string))
def parse_resolution_string(resolution_string): if resolution_string == '720p': return 1280,720 if resolution_string == '1080p': return 1920,1080 for separator in (':', 'x', ","): if separator in resolution_string: width, _, height = resolution_string.partition(separator) try: return int(width), int(height) except ValueError: raise PrassError("Invalid resolution string: '{0}'".format(resolution_string)) raise PrassError("Invalid resolution string: '{0}'".format(resolution_string))
def parse_fps_string(fps_string): if '/' in fps_string: parts = fps_string.split('/') if len(parts) > 2: raise PrassError('Invalid fps value') try: return float(parts[0]) / float(parts[1]) except ValueError: raise PrassError('Invalid fps value') else: try: return float(fps_string) except ValueError: raise PrassError('Invalid fps value')
def shift(input_file, output_file, shift_by, shift_start, shift_end, multiplier): """Shift all lines in a script by defined amount and/or change speed. \b You can use one of the following formats to specify the time for shift: - "1.5s" or just "1.5" means 1 second 500 milliseconds - "150ms" means 150 milliseconds - "1:7:12.55" means 1 hour, 7 minutes, 12 seconds and 550 milliseconds. All parts are optional. Every format allows a negative sign before the value, which means "shift back", like "-12s" \b Optionally, specify multiplier to change speed: - 1.2 makes subs 20% faster - 7/8 makes subs 12.5% slower \b To shift both start end end time by one minute and 15 seconds: $ prass shift input.ass --by 1:15 -o output.ass To shift only start time by half a second back: $ prass shift input.ass --start --by -0.5s -o output.ass """ if not shift_start and not shift_end: shift_start = shift_end = True shift_ms = parse_shift_string(shift_by) multiplier = parse_fps_string(multiplier) if multiplier < 0: raise PrassError('Speed multiplier should be a positive number') script = AssScript.from_ass_stream(input_file) script.shift(shift_ms, shift_start, shift_end, multiplier) script.to_ass_stream(output_file)
def tpp(input_file, output_file, styles, lead_in, lead_out, max_overlap, max_gap, adjacent_bias, keyframes_path, timecodes_path, fps, kf_before_start, kf_after_start, kf_before_end, kf_after_end): """Timing post-processor. It's a pretty straightforward port from Aegisub so you should be familiar with it. You have to specify keyframes and timecodes (either as a CFR value or a timecodes file) if you want keyframe snapping. All parameters default to zero so if you don't want something - just don't put it in the command line. \b To add lead-in and lead-out: $ prass tpp input.ass --lead-in 50 --lead-out 150 -o output.ass To make adjacent lines continuous, with 80% bias to changing end time of the first line: $ prass tpp input.ass --overlap 50 --gap 200 --bias 80 -o output.ass To snap events to keyframes without a timecodes file: $ prass tpp input.ass --keyframes kfs.txt --fps 23.976 --kf-before-end 150 --kf-after-end 150 --kf-before-start 150 --kf-after-start 150 -o output.ass """ if fps and timecodes_path: raise PrassError( 'Timecodes file and fps cannot be specified at the same time') if fps: timecodes = Timecodes.cfr(parse_fps_string(fps)) elif timecodes_path: timecodes = Timecodes.from_file(timecodes_path) elif any((kf_before_start, kf_after_start, kf_before_end, kf_after_end)): raise PrassError( 'You have to provide either fps or timecodes file for keyframes processing' ) else: timecodes = None if timecodes and not keyframes_path: raise PrassError( 'You have to specify keyframes file for keyframes processing') keyframes_list = parse_keyframes( keyframes_path) if keyframes_path else None actual_styles = [] for style in styles: actual_styles.extend(x.strip() for x in style.split(',')) script = AssScript.from_ass_stream(input_file) script.tpp(actual_styles, lead_in, lead_out, max_overlap, max_gap, adjacent_bias, keyframes_list, timecodes, kf_before_start, kf_after_start, kf_before_end, kf_after_end) script.to_ass_stream(output_file)
def from_ass_stream(cls, file_object): sections = [] current_section = None force_last_section = False for idx, line in enumerate(file_object): line = line.strip() # required because a line might be both a part of an attachment and a valid header if force_last_section: try: force_last_section = current_section.parse_line(line) continue except Exception as e: raise PrassError( u"That's some invalid ASS script: {0}".format( e.message)) if not line: continue low = line.lower() if low == u'[v4+ styles]': current_section = StylesSection() sections.append((line, current_section)) elif low == u'[events]': current_section = EventsSection() sections.append((line, current_section)) elif low == u'[script info]': current_section = ScriptInfoSection() sections.append((line, current_section)) elif low == u'[graphics]' or low == u'[fonts]': current_section = AttachmentSection() sections.append((line, current_section)) elif re.match(r'^\s*\[.+?\]\s*$', low): current_section = GenericSection() sections.append((line, current_section)) elif not current_section: raise PrassError( u"That's some invalid ASS script (no parse function at line {0})" .format(idx)) else: try: force_last_section = current_section.parse_line(line) except Exception as e: raise PrassError( u"That's some invalid ASS script: {0}".format( e.message)) return cls(sections)
def parse_keyframes(path): with open(path) as file_object: text = file_object.read() if text.find('# XviD 2pass stat file')>=0: frames = parse_scxvid_keyframes(text) else: raise PrassError('Unsupported keyframes type') if 0 not in frames: frames.insert(0, 0) return frames
def parse_srt_time(string): m = re.match(r"(\d+):(\d+):(\d+)\,(\d+)", string) if m != None: groups = m.groups() hours, minutes, seconds, milliseconds = list(map(int, groups)) return hours * 3600000 + minutes * 60000 + seconds * 1000 + milliseconds else: raise PrassError( "____\nFailed to parse ass time in the following line:\n{0}\n____". format(string)) return None
def convert_srt(input_path, output_file, encoding): """Convert SRT script to ASS. \b Example: $ prass convert-srt input.srt -o output.ass --encoding cp1251 """ try: with click.open_file(input_path, encoding=encoding) as input_file: AssScript.from_srt_stream(input_file).to_ass_stream(output_file) except LookupError: raise PrassError("Encoding {0} doesn't exist".format(encoding))
def parse(cls, text): lines = text.splitlines() if not lines: return [] first = lines[0].lower().lstrip() if first.startswith('# timecode format v2'): tcs = [x for x in lines[1:]] return Timecodes(tcs, None) elif first.startswith('# timecode format v1'): default = float(lines[1].lower().replace('assume ', "")) overrides = (x.split(',') for x in lines[2:]) return Timecodes(cls._convert_v1_to_v2(default, overrides), default) else: raise PrassError('This timecodes format is not supported')
def tpp(self, styles, lead_in, lead_out, max_overlap, max_gap, adjacent_bias, keyframes_list, timecodes, kf_before_start, kf_after_start, kf_before_end, kf_after_end): def get_closest_kf(frame, keyframes): idx = bisect.bisect_left(keyframes, frame) if idx == len(keyframes): return keyframes[-1] if idx == 0 or keyframes[idx] - frame < frame - (keyframes[idx - 1]): return keyframes[idx] return keyframes[idx - 1] events_iter = (e for e in self._events if not e.is_comment) if styles: styles = set(s.lower() for s in styles) events_iter = (e for e in events_iter if e.style.lower() in styles) events_list = sorted(events_iter, key=lambda x: x.start) broken = next((e for e in events_list if e.start > e.end), None) if broken: raise PrassError( "One of the lines in the file ({0}) has negative duration. Aborting." .format(broken)) if lead_in: sorted_by_end = sorted(events_list, key=lambda x: x.end) for idx, event in enumerate(sorted_by_end): initial = max(event.start - lead_in, 0) for other in reversed(sorted_by_end[:idx]): if other.end <= initial: break if not event.collides_with(other): initial = max(initial, other.end) event.start = initial if lead_out: for idx, event in enumerate(events_list): initial = event.end + lead_out for other in events_list[idx:]: if other.start > initial: break if not event.collides_with(other): initial = min(initial, other.start) event.end = initial if max_overlap or max_gap: bias = adjacent_bias / 100.0 for previous, current in zip(events_list, events_list[1:]): distance = current.start - previous.end if (distance < 0 and -distance <= max_overlap) or ( distance > 0 and distance <= max_gap): new_time = previous.end + distance * bias current.start = new_time previous.end = new_time if kf_before_start or kf_after_start or kf_before_end or kf_after_end: for event in events_list: start_frame = timecodes.get_frame_number( event.start, timecodes.TIMESTAMP_START) end_frame = timecodes.get_frame_number(event.end, timecodes.TIMESTAMP_END) closest_frame = get_closest_kf(start_frame, keyframes_list) closest_time = timecodes.get_frame_time( closest_frame, timecodes.TIMESTAMP_START) if (end_frame > closest_frame >= start_frame and closest_time - event.start <= kf_after_start) or \ (closest_frame <= start_frame and event.start - closest_time <= kf_before_start): event.start = max(0, closest_time) closest_frame = get_closest_kf(end_frame, keyframes_list) - 1 closest_time = timecodes.get_frame_time( closest_frame, timecodes.TIMESTAMP_END) if (start_frame < closest_frame <= end_frame and event.end - closest_time <= kf_before_end) or \ (closest_frame >= end_frame and closest_time - event.end <= kf_after_end): event.end = closest_time
def from_ass_file(cls, path): try: with codecs.open(path, encoding='utf-8-sig') as script: return cls.from_ass_stream(script) except IOError: raise PrassError("Script {0} not found".format(path))