padmafcp/srt2fcp.py
2021-11-05 10:36:21 +00:00

373 lines
10 KiB
Python
Executable file

#!/usr/bin/env python3
import sys
import os
from optparse import OptionParser
import json
import math
import shutil
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
import uuid
import xml.sax.saxutils
import ox
from PIL import Image
base = os.path.abspath(os.path.dirname(__file__))
generator_template = '''<generatoritem id="Text%(id)s">
<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>%(font)s</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>$(uuid)s</uuid>
</itemhistory>
</generatoritem>
'''
fcp_header = '''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xmeml>
<xmeml version="5">'''
fcp_footer = '''</xmeml>'''
sequence_template = '''
<sequence id="%(id)s ">
<uuid>$(uuid)s</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>1920</width>
<height>1080</height>
<anamorphic>FALSE</anamorphic>
<pixelaspectratio>square</pixelaspectratio>
<fielddominance>none</fielddominance>
<rate>
<ntsc>FALSE</ntsc>
<timebase>25</timebase>
</rate>
<colordepth>24</colordepth>
<codec>
<name>Apple ProRes 422</name>
<appspecificdata>
<appname>Final Cut Pro</appname>
<appmanufacturer>Apple Inc.</appmanufacturer>
<appversion>7.0</appversion>
<data>
<qtcodec>
<codecname>Apple ProRes 422</codecname>
<codectypename>Apple ProRes 422</codectypename>
<codectypecode>apcn</codectypecode>
<codecvendorcode>appl</codecvendorcode>
<spatialquality>1024</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 = 1920 - 240
font = os.path.join(base, 'DejaVuSansCondensedBold.ttf')
if not os.path.exists(font):
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
font = 'Courier New'
#font = 'Lucida Grande'
gid = 0
def __init__(self, size=None, font=None):
if size is not None:
self.fontsize = size
if font is not None:
self.font = font
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],
'uuid': str(uuid.uuid1()).upper(),
'duration': duration,
'subs': '\n'.join([self.sub(*s) for s in subs])
})
def sub(self, start, end, text):
self.gid += 1
text = xml.sax.saxutils.escape('\n'.join(text).strip()).replace('\n', '&#13;')
return generator_template % {
'id': self.gid,
'uuid': str(uuid.uuid1()).upper(),
'start': start,
'end': end,
'duration': end-start,
'text': text,
'fontsize': self.fontsize,
'font': self.font,
}
def save(self, output):
with open(output, 'w') as f:
f.write(fcp_header)
for s in self.sequences:
f.write(s)
f.write(fcp_footer)
if __name__ == '__main__':
usage = "usage: %prog srtfile"
parser = OptionParser(usage=usage)
parser.add_option('-w', '--wrap', dest='wrap', help='rewrap text', action="store_true")
parser.add_option('-s', '--size', dest='size', help='font size, default=18', type=int, default=18)
parser.add_option('-f', '--font', dest='font', help='font name, default="Courier New"', type=str, default='Courier New')
(opts, args) = parser.parse_args()
if not args:
parser.print_help()
sys.exit()
srt = args[0]
output = srt.replace('.srt', '') + '.xml'
fcp = Fcp(font=opts.font, size=int(opts.size))
fcp.add_srt(srt, opts.wrap)
fcp.save(output)