commit 00521c3dc49f3b83121cf38954b6c1d41aead9bd Author: j Date: Sun Sep 9 14:09:55 2018 +0200 fcp2srt diff --git a/DejaVuSansCondensedBold.ttf b/DejaVuSansCondensedBold.ttf new file mode 100644 index 0000000..cb48a02 Binary files /dev/null and b/DejaVuSansCondensedBold.ttf differ diff --git a/fcp2edit.py b/fcp2edit.py new file mode 100755 index 0000000..cc5d327 --- /dev/null +++ b/fcp2edit.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python +from __future__ import division + +import lxml +import lxml.etree +import ox +import os +import json +import sys +import re + +source = sys.argv[1] +data = [] +tree = lxml.etree.parse(source) + +api = ox.API('https://pad.ma/api/') + +if os.path.exists('sot.json'): + sot = json.load(open('sot.json')) +else: + sot = api.find({ + 'query': { + 'conditions': [{'key': 'list', 'value': 'zi:SOT'}] + }, + 'keys': ['id', 'title', 'duration'], + 'range': [0, 5000] + })['data']['items'] + with open('sot.json', 'w') as f: + json.dump(sot, f, indent=2) + +def get_item(id): + id = id.replace('_', ' ').replace('.MOV', '').replace('BT2C0662 3', 'BT2C0662') + for data in sot: + if id in data['title'].replace('/', ' '): + return data['id'] + for k in ('STK', 'MNK', 'BT2C'): + id = id.replace(k, '').strip() + for data in sot: + if id in data['title'].replace('/', ' '): + return data['id'] + print 'missing', id + +durations = {data['id']: data['duration'] for data in sot} + +def parse_fps(rate): + if rate.find('ntsc').text == 'TRUE': + rate = int(rate.find('timebase').text) * 1000 / 1001 + else: + rate = int(rate.find('timebase').text) + return rate + +tracks = [] +seq = tree.getroot()[0] +fps = parse_fps(seq.find('rate')) +v = seq.find('media').find('video') +for t in v.xpath('.//track'): + track = [] + for clipitem in t.xpath('.//clipitem'): + _id = clipitem.attrib['id'].strip().split('-Apple')[0] + _id = _id.replace('Shampoo flask', '').replace(' HDR', '').replace(' hdr', '').replace('pro res 422', '').replace(' cool', '').strip() + _id = re.sub(' \d\d?$', '', _id) + + #start/end - position on timeline + #in/out - in/out in clip + clip_fps = parse_fps(clipitem.find('rate')) + _in = int(clipitem.findall('in')[0].text) + _out = int(clipitem.findall('out')[0].text) + duration = _out - _in + _start = int(clipitem.findall('start')[0].text) + _end = int(clipitem.findall('end')[0].text) + if _start == -1 and _end == -1: + print 'strange', _start, _end, _in, _out, _id + continue + if _start == -1: + _start = _end - duration + elif _end == -1: + _end = _start + duration + if filter(lambda x: x <0, [_start, _end, _in, _out]): + print 'why -?', _start, _end, _in, _out, _id + if _out - _in != _end - _start: + print '??', _in, _out, _out-_in, 'vs', _start, _end, _end-_start, _id + if _start > -1 and _end > -1: + track.append({ + 'in': _in / fps, + 'out': _out / fps, + 'start': _start / fps, + 'end': _end / fps, + 'file': _id, + 'id': get_item(_id), + 'track': len(tracks) + }) + if track: + tracks.append(track) + +with open('/tmp/tracks.json', 'w') as f: + json.dump(tracks, f, indent=2, sort_keys=True) + +for i, track in enumerate(tracks): + with open('/tmp/tracks%s.json' % i, 'w') as f: + json.dump(track, f, indent=2, sort_keys=True) + +def flatten_tracks(tracks): + def split_at_overlaps(clip): + offset_start = clip['start'] + offset_clip = clip['in'] + points = [clip['start'], clip['end']] + for track in tracks: + for c in track: + if c['track'] != clip['track'] and c['start'] > -1 and c['end'] > -1: + if c['start' ] > clip['start'] and c['start'] < clip['end']: + points.append(c['start']) + if c['end' ] > clip['start'] and c['end'] < clip['end']: + points.append(c['end']) + print clip['track'], points + clips = [] + for i, point in enumerate(points[:-1]): + offset_in = point - offset_start + duration = points[i+1] - point + if duration > 0: + clips.append({ + 'in': offset_clip + offset_in, + 'out': offset_clip + offset_in + duration, + 'start': point, + 'end': points[i+1], + 'track': clip['track'], + 'id': clip['id'] + }) + return clips + + clips = [] + for track in tracks: + for clip in track: + clips += split_at_overlaps(clip) + for clip in clips: + for c in clips: + if c['track'] > clip['track']: + if c['start'] <= clip['start'] and c['end'] >= clip['end']: + clip['delete'] = True + _clips = sorted([c for c in clips if not c.get('delete')], key=lambda a: a['start']) + clips = [] + for c in _clips: + if clips and clips[-1]['out'] == c['in'] and clips[-1]['id'] == c['id']: + print 'join', clips[-1], c + clips[-1]['end'] = c['end'] + clips[-1]['out'] = c['out'] + else: + clips.append(c) + position = None + for c in clips: + if position == None: + position = c['start'] + if c['start'] != position: + print 'wrong start', c['start'], position, abs(position - c['start']) + position += c['out'] - c['in'] + return clips + +timeline = flatten_tracks(tracks) +pandora_edit = [] +for c in timeline: + if c['id']: + if c['out'] > durations[c['id']] or c['in'] > durations[c['id']] or c['in'] < 0 or c['out'] <= c['in']: + print 'invalid in/out', c, durations[c['id']] + else: + pandora_edit.append({ + 'in': c['in'], + 'out': c['out'], + 'item': c['id'] + }) + +''' +''' +print len(pandora_edit) +#print json.dumps(timeline, indent=2, sort_keys=True) +print json.dumps(pandora_edit, indent=2, sort_keys=True) +with open(os.path.expanduser('~/.ox/client.padma.json')) as f: + settings = json.load(f) +r = api.signin(username=settings['username'], password=settings['password']) +assert(r['status']['code'] == 200) +print 'add clips', len(pandora_edit) +#print pandora_edit +#r = api.addEdit({ +# 'name': 'Ship of Theseus', +# 'clips': pandora_edit +#}) +#print r['data'].get('id') or r +clips = [c['id'] for c in api.getEdit({'id': 'j:Ship of Theseus', 'keys': ['clips']})['data']['clips']] +if clips: + api.removeClips({ + 'edit': 'j:Ship of Theseus', + 'ids': clips + }) +step = 100 +while pandora_edit: + clips = pandora_edit[:step] + pandora_edit = pandora_edit[step:] + print 'add', len(clips), 'todo', len(pandora_edit) + r = api.addClips({ + 'edit': 'j:Ship of Theseus', + 'clips': clips + }) + print 'total added', len(r and r.get('data', {}).get('clips') or 0) diff --git a/fcp2json.py b/fcp2json.py new file mode 100755 index 0000000..eda254b --- /dev/null +++ b/fcp2json.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +from __future__ import division + +import lxml +import lxml.etree +import ox +import os +import json +import sys + +fps = 25 +tree = lxml.etree.parse(sys.argv[1]) +source = sys.argv[1] +target = '%s.json' % os.path.splitext(source)[0] +data = { + 'descriptions': [], + 'transcripts': [] +} +for g in tree.xpath('//generatoritem'): + #start/end = relative position of the clip in the parent sequence. + #in/out indicate the portion of the source media file to reference. + _in = int(g.findall('in')[0].text) + _out = int(g.findall('out')[0].text) + _start = int(g.findall('start')[0].text) + _end = int(g.findall('end')[0].text) + effect = g.findall('effect') + assert len(effect) == 1 + for parameter in effect[0].findall('parameter'): + if parameter.findall('parameterid')[0].text == 'str': + value = parameter.findall('value')[0].text + + if _start == -1 and _end == -1: + _start = _in + _end = _out + if _start == -1: + _start = 0 + #print _in, _out, _start, _end, value + value = '
\n'.join([v.strip() for v in value.strip().split('\r')]) + data['transcripts'].append({ + 'in': _start/fps, 'out': (_end-1)/fps, 'value': value + }) + +_last = 0 +for g in tree.xpath('//clipitem'): + #in/out indicate the portion of the source media file to reference. + #start/end = relative position of the clip in the parent sequence. + _in = int(g.findall('in')[0].text) / fps + _out = int(g.findall('out')[0].text) / fps + _start = int(g.findall('start')[0].text) / fps + _end = int(g.findall('end')[0].text) / fps + name= g.findall('name')[0].text.strip() + #print _in, _out, _start, _end, name + if _start == -0.04: + _start = _last + if _end == -0.04: + _end = _start + (_out - _in) + name = name.replace('.dv', '').replace('_ ', ': ') + id = name.replace(' ', '%20') + value = 'Source: %s/%s-%s' % (id, _in, _out, name, ox.formatDuration(_in), ox.formatDuration(_out)) + data['descriptions'].append({ + 'in': _start, 'out': _end-0.04, 'value': value + }) + _last = _end + +with open(target, 'w') as f: + json.dump(data, f, indent=2) + +''' +import os +import ox + +with open(os.path.expanduser('~/.ox/client.json')) as f: + config = json.load(f) + +api = ox.API('https://pad.ma/api/') +r = api.signin(username=config['username'], password=config['password']) +assert(r['status']['code'] == 200) +assert(r['data']['user'] != '') +for s in data['descriptions']: + s['item'] = 'BHK' + s['layer'] = 'descriptions' + print s + r = api.addAnnotation(s) + assert(r['status']['code'] == 200) +for s in data['transcripts']: + s['item'] = 'BHK' + s['layer'] = 'transcripts' + print s + r = api.addAnnotation(s) + assert(r['status']['code'] == 200) +''' diff --git a/fcp2srt.py b/fcp2srt.py new file mode 100755 index 0000000..c29b2c9 --- /dev/null +++ b/fcp2srt.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +from __future__ import division + +import lxml +import lxml.etree +import ox +import os +import json +import sys + +source = sys.argv[1] +target = '%s.srt' % os.path.splitext(source)[0] +fps = 25 +data = [] +tree = lxml.etree.parse(source) + +for g in tree.xpath('//generatoritem'): + #start/end = relative position of the clip in the parent sequence. + #in/out indicate the portion of the source media file to reference. + _in = int(g.findall('in')[0].text) + _out = int(g.findall('out')[0].text) + _start = int(g.findall('start')[0].text) + _end = int(g.findall('end')[0].text) + effect = g.findall('effect') + assert len(effect) == 1 + for parameter in effect[0].findall('parameter'): + if parameter.findall('parameterid')[0].text == 'str': + value = parameter.findall('value')[0].text + + if _start == -1 and _end == -1: + _start = _in + _end = _out + if _start == -1: + _start = 0 + #print _in, _out, _start, _end, value + value = '\n'.join([v.strip() for v in value.strip().split('\r')]) + value = value.replace('\n\n', '

\n') + data.append({ + 'in': _start/fps, 'out': (_end-1)/fps, 'value': value + }) + +with open(target, 'w') as f: + f.write(ox.srt.encode(data)) diff --git a/srt2fcp.py b/srt2fcp.py new file mode 100755 index 0000000..8241b92 --- /dev/null +++ b/srt2fcp.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python +from __future__ import division +import sys +import os +from optparse import OptionParser +import tempfile +import shutil +import math +import time +import json +import urllib +import xml.sax.saxutils + +import ox +import Image + +base = os.path.abspath(os.path.dirname(__file__)) + +generator_template = u''' +Text +3000 + + FALSE + 25 + +0 +%(duration)s +%(start)s +%(end)s +TRUE +FALSE +black + + Text + Text + Text + generator + video + + str + Text + %(text)s + + + fontname + Font + Courier New + + + fontsize + Size + 0 + 1000 + %(fontsize)s + + + fontstyle + Style + 1 + 4 + + + Plain + 1 + + + Bold + 2 + + + Italic + 3 + + + Bold/Italic + 4 + + + 1 + + + fontalign + Alignment + 1 + 3 + + + Left + 1 + + + Center + 2 + + + Right + 3 + + + 1 + + + fontcolor + Font Color + + 255 + 255 + 255 + 255 + + + + origin + Origin + + -0.402778 + -0.217014 + + + + fonttrack + Tracking + -200 + 200 + 1 + + + leading + Leading + -100 + 100 + 0 + + + aspect + Aspect + 0.1 + 5 + 1 + + + autokern + Auto Kerning + TRUE + + + subpixel + Use Subpixel + TRUE + + + + + Basic Motion + basic + motion + motion + video + + center + Center + + 0 + -0.00315457 + + + + + + video + + + BAA51DEC-ECB0-4879-9910-8E83B0EF7C1B + + +''' + +fcp_header = u''' + +''' +fcp_footer = u'''''' + +sequence_template = u''' + + 72DC4146-6224-4400-BAAC-2AB6E0D3D292 + add + %(id)s + %(duration)s + + FALSE + 25 + + + + FALSE + 25 + + 01:00:00:00 + 90000 + source + NDF + + -1 + -1 + + + + FALSE +''' + +def wrap_text(start, end, text, fontsize=30): + margin = 40 + width = 640 + #font = os.path.join(base, 'DejaVuSansCondensedBold.ttf') + font = '/usr/share/fonts/truetype/ttf-dejavu/DejaVuSansMono-Bold.ttf' + + n = 10 + alltxt = [] + text = ox.strip_tags(ox.decode_html(text)) + for t in text.strip().split('\n'): + lines = ox.wrapText(t, width - 2 * margin, 20, font, fontsize) + for line in lines: + alltxt.append(line) + alltxt.append('') + + pages = int(math.ceil(len(alltxt) / n)) + lines_per_page = int(math.ceil(len(alltxt) / pages)) + frames_per_page = int((end - start) / pages) + pos = start + r = [] + for p in range(0, len(alltxt), lines_per_page): + txt = alltxt[p:p+lines_per_page] + r.append((pos, pos+frames_per_page, txt)) + pos += frames_per_page + return r + +class Fcp: + sequences = [] + fps = 25 + fontsize = 18 + + def __init__(self): + pass + + def add_srt(self, srt, wrap=True): + data = ox.srt.load(srt) + #info = api.get(id=item_id, keys=['layers']) + subs = [] + #for s in info['data']['layers']['transcripts']: + duration = -1 + for s in data: + value = s['value'].replace('
', '\n').replace('
', '\n') + value = ox.strip_tags(value) + start = int(s['in'] * fcp.fps) + end = int(s['out'] * fcp.fps) + if start < duration: + print "warning", start, '<', duration, value + start = duration + duration = end + + if wrap: + for t in wrap_text(start, end, value, self.fontsize): + subs.append(t) + else: + subs.append((start, end, value.split('\n'))) + self.sequences.append(sequence_template % { + 'id': os.path.splitext(os.path.basename(srt))[0], + 'duration': duration, + 'subs': '\n'.join([self.sub(*s) for s in subs]) + }) + + def sub(self, start, end, text): + text = xml.sax.saxutils.escape('\n'.join(text).strip()).replace('\n', ' ') + return generator_template % { + 'start': start, + 'end': end, + 'duration': end-start, + 'text': text, + 'fontsize': self.fontsize, + } + + def save(self, output): + with open(output, 'w') as f: + f.write(fcp_header.encode('utf-8')) + for s in self.sequences: + f.write(s.encode('utf-8')) + f.write(fcp_footer.encode('utf-8')) + +if __name__ == '__main__': + + usage = "usage: %prog srtfile" + parser = OptionParser(usage=usage) + parser.add_option('-w', '--wrap', dest='wrap', help='rewrap text', action="store_true") + (opts, args) = parser.parse_args() + + if not args: + parser.print_help() + sys.exit() + + srt = args[0] + output = srt.replace('.srt', '') + '.xml' + fcp = Fcp() + fcp.add_srt(srt, opts.wrap) + fcp.save(output)