This commit is contained in:
j 2018-09-09 14:09:55 +02:00
commit 00521c3dc4
5 changed files with 691 additions and 0 deletions

BIN
DejaVuSansCondensedBold.ttf Normal file

Binary file not shown.

201
fcp2edit.py Executable file
View File

@ -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)

91
fcp2json.py Executable file
View File

@ -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 = '<br>\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: <a href="/%s/%.3f,%.3f">%s/%s-%s</a>' % (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)
'''

43
fcp2srt.py Executable file
View File

@ -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', '<br><br>\n')
data.append({
'in': _start/fps, 'out': (_end-1)/fps, 'value': value
})
with open(target, 'w') as f:
f.write(ox.srt.encode(data))

356
srt2fcp.py Executable file
View File

@ -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'''<generatoritem id="Text">
<name>Text</name>
<duration>3000</duration>
<rate>
<ntsc>FALSE</ntsc>
<timebase>25</timebase>
</rate>
<in>0</in>
<out>%(duration)s</out>
<start>%(start)s</start>
<end>%(end)s</end>
<enabled>TRUE</enabled>
<anamorphic>FALSE</anamorphic>
<alphatype>black</alphatype>
<effect>
<name>Text</name>
<effectid>Text</effectid>
<effectcategory>Text</effectcategory>
<effecttype>generator</effecttype>
<mediatype>video</mediatype>
<parameter>
<parameterid>str</parameterid>
<name>Text</name>
<value>%(text)s</value>
</parameter>
<parameter>
<parameterid>fontname</parameterid>
<name>Font</name>
<value>Courier New</value>
</parameter>
<parameter>
<parameterid>fontsize</parameterid>
<name>Size</name>
<valuemin>0</valuemin>
<valuemax>1000</valuemax>
<value>%(fontsize)s</value>
</parameter>
<parameter>
<parameterid>fontstyle</parameterid>
<name>Style</name>
<valuemin>1</valuemin>
<valuemax>4</valuemax>
<valuelist>
<valueentry>
<name>Plain</name>
<value>1</value>
</valueentry>
<valueentry>
<name>Bold</name>
<value>2</value>
</valueentry>
<valueentry>
<name>Italic</name>
<value>3</value>
</valueentry>
<valueentry>
<name>Bold/Italic</name>
<value>4</value>
</valueentry>
</valuelist>
<value>1</value>
</parameter>
<parameter>
<parameterid>fontalign</parameterid>
<name>Alignment</name>
<valuemin>1</valuemin>
<valuemax>3</valuemax>
<valuelist>
<valueentry>
<name>Left</name>
<value>1</value>
</valueentry>
<valueentry>
<name>Center</name>
<value>2</value>
</valueentry>
<valueentry>
<name>Right</name>
<value>3</value>
</valueentry>
</valuelist>
<value>1</value>
</parameter>
<parameter>
<parameterid>fontcolor</parameterid>
<name>Font Color</name>
<value>
<alpha>255</alpha>
<red>255</red>
<green>255</green>
<blue>255</blue>
</value>
</parameter>
<parameter>
<parameterid>origin</parameterid>
<name>Origin</name>
<value>
<horiz>-0.402778</horiz>
<vert>-0.217014</vert>
</value>
</parameter>
<parameter>
<parameterid>fonttrack</parameterid>
<name>Tracking</name>
<valuemin>-200</valuemin>
<valuemax>200</valuemax>
<value>1</value>
</parameter>
<parameter>
<parameterid>leading</parameterid>
<name>Leading</name>
<valuemin>-100</valuemin>
<valuemax>100</valuemax>
<value>0</value>
</parameter>
<parameter>
<parameterid>aspect</parameterid>
<name>Aspect</name>
<valuemin>0.1</valuemin>
<valuemax>5</valuemax>
<value>1</value>
</parameter>
<parameter>
<parameterid>autokern</parameterid>
<name>Auto Kerning</name>
<value>TRUE</value>
</parameter>
<parameter>
<parameterid>subpixel</parameterid>
<name>Use Subpixel</name>
<value>TRUE</value>
</parameter>
</effect>
<filter>
<effect>
<name>Basic Motion</name>
<effectid>basic</effectid>
<effectcategory>motion</effectcategory>
<effecttype>motion</effecttype>
<mediatype>video</mediatype>
<parameter>
<parameterid>center</parameterid>
<name>Center</name>
<value>
<horiz>0</horiz>
<vert>-0.00315457</vert>
</value>
</parameter>
</effect>
</filter>
<sourcetrack>
<mediatype>video</mediatype>
</sourcetrack>
<itemhistory>
<uuid>BAA51DEC-ECB0-4879-9910-8E83B0EF7C1B</uuid>
</itemhistory>
</generatoritem>
'''
fcp_header = u'''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xmeml>
<xmeml version="5">'''
fcp_footer = u'''</xmeml>'''
sequence_template = u'''
<sequence id="%(id)s ">
<uuid>72DC4146-6224-4400-BAAC-2AB6E0D3D292</uuid>
<updatebehavior>add</updatebehavior>
<name>%(id)s</name>
<duration>%(duration)s</duration>
<rate>
<ntsc>FALSE</ntsc>
<timebase>25</timebase>
</rate>
<timecode>
<rate>
<ntsc>FALSE</ntsc>
<timebase>25</timebase>
</rate>
<string>01:00:00:00</string>
<frame>90000</frame>
<source>source</source>
<displayformat>NDF</displayformat>
</timecode>
<in>-1</in>
<out>-1</out>
<media>
<video>
<format>
<samplecharacteristics>
<width>720</width>
<height>576</height>
<anamorphic>FALSE</anamorphic>
<pixelaspectratio>PAL-601</pixelaspectratio>
<fielddominance>none</fielddominance>
<rate>
<ntsc>FALSE</ntsc>
<timebase>25</timebase>
</rate>
<colordepth>24</colordepth>
<codec>
<name>Apple DV - PAL</name>
<appspecificdata>
<appname>Final Cut Pro</appname>
<appmanufacturer>Apple Inc.</appmanufacturer>
<appversion>7.0</appversion>
<data>
<qtcodec>
<codecname>Apple DV - PAL</codecname>
<codectypename>DV - PAL</codectypename>
<codectypecode>dvcp</codectypecode>
<codecvendorcode>appl</codecvendorcode>
<spatialquality>1023</spatialquality>
<temporalquality>0</temporalquality>
<keyframerate>0</keyframerate>
<datarate>0</datarate>
</qtcodec>
</data>
</appspecificdata>
</codec>
</samplecharacteristics>
<appspecificdata>
<appname>Final Cut Pro</appname>
<appmanufacturer>Apple Inc.</appmanufacturer>
<appversion>7.0</appversion>
<data>
<fcpimageprocessing>
<useyuv>TRUE</useyuv>
<usesuperwhite>FALSE</usesuperwhite>
<rendermode>YUV8BPP</rendermode>
</fcpimageprocessing>
</data>
</appspecificdata>
</format>
<track>
%(subs)s
<enabled>TRUE</enabled>
<locked>FALSE</locked>
</track>
</video>
</media>
<ismasterclip>FALSE</ismasterclip>
</sequence>'''
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('<br/>', '\n').replace('<br>', '\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', '&#13;&#10;')
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)