Merge branch 'feat/refactor-make-ffmpeg'

This commit is contained in:
Jannik Beyerstedt 2024-11-03 21:35:20 +01:00
commit b3fb07a6e3
12 changed files with 348 additions and 654 deletions

View file

@ -1,15 +1,13 @@
[default] [meta]
schedule = https://pretalx.c3voc.de/camp2023/schedule/export/schedule.xml schedule = https://pretalx.c3voc.de/camp2023/schedule/export/schedule.xml
template = intro.mp4 template = intro.mp4
alpha = false alpha = false
prores = false prores = false
fontfile = true inout_type = t
inout = t
[title] [title]
in = 16 in = 16
out = 24 out = 24
fontfamily = BeonRegular
fontfile = saira.ttf fontfile = saira.ttf
fontsize = 100 fontsize = 100
fontcolor = #FB48C4 fontcolor = #FB48C4
@ -19,7 +17,6 @@ y = 100
[speaker] [speaker]
in = 16 in = 16
out = 24 out = 24
fontfamily = BeonRegular
fontfile = saira.ttf fontfile = saira.ttf
fontsize = 70 fontsize = 70
fontcolor = #3FFF21 fontcolor = #3FFF21
@ -29,11 +26,10 @@ y = 800
[text] [text]
in = 16 in = 16
out = 24 out = 24
fontfamily = BeonRegular
fontfile = saira.ttf fontfile = saira.ttf
fontsize = 45 fontsize = 45
fontcolor = #FB48C4 fontcolor = #FB48C4
x = (w-text_w)/2 x = (w-text_w)/2
y = 1000 y = 1000
text = 'Chaos Communication Camp 2023' text = Chaos Communication Camp 2023

View file

@ -1,15 +1,13 @@
[default] [meta]
schedule = https://fahrplan.events.ccc.de/camp/2019/Fahrplan/schedule.xml schedule = https://fahrplan.events.ccc.de/camp/2019/Fahrplan/schedule.xml
template = cccamp19_talks_intro_1080p.mov template = cccamp19_talks_intro_1080p.mov
alpha = true alpha = true
prores = true prores = true
fontfile = true inout_type = n
inout = n
[title] [title]
in = 193 in = 193
out = 324 out = 324
fontfamily =
fontfile = Marvel-Bold.ttf fontfile = Marvel-Bold.ttf
fontsize = 120 fontsize = 120
fontcolor = #c68100 fontcolor = #c68100
@ -19,7 +17,6 @@ y = 480
[speaker] [speaker]
in = 233 in = 233
out = 324 out = 324
fontfamily =
fontfile = Marvel-Regular.ttf fontfile = Marvel-Regular.ttf
fontsize = 70 fontsize = 70
fontcolor = #c68100 fontcolor = #c68100
@ -29,11 +26,10 @@ y = 845
[text] [text]
in = 242 in = 242
out = 324 out = 324
fontfamily =
fontfile = Marvel-Regular.ttf fontfile = Marvel-Regular.ttf
fontsize = 45 fontsize = 45
fontcolor = #c68100 fontcolor = #c68100
x = (w-text_w)/2 x = (w-text_w)/2
y = 927 y = 927
text = 'chaos communication camp 2019' text = chaos communication camp 2019

View file

@ -1,15 +1,13 @@
[default] [meta]
schedule = https://pretalx.denog.de/denog11/schedule/export/schedule.xml schedule = https://pretalx.denog.de/denog11/schedule/export/schedule.xml
template = denog11_intro_template.ts template = denog11_intro_template.ts
alpha = false alpha = false
prores = false prores = false
fontfile = true inout_type = n
inout = n
[title] [title]
in = 1 in = 1
out = 6.5 out = 6.5
fontfamily =
fontfile = DejaVuSans.ttf fontfile = DejaVuSans.ttf
fontsize = 100 fontsize = 100
fontcolor = #f9cc12 fontcolor = #f9cc12
@ -19,7 +17,6 @@ y = 200
[speaker] [speaker]
in = 2 in = 2
out = 6.5 out = 6.5
fontfamily =
fontfile = DejaVuSans.ttf fontfile = DejaVuSans.ttf
fontsize = 60 fontsize = 60
fontcolor = #ffffff fontcolor = #ffffff
@ -29,11 +26,10 @@ y = 900
[text] [text]
in = 3 in = 3
out = 6.5 out = 6.5
fontfamily =
fontfile = DejaVuSans.ttf fontfile = DejaVuSans.ttf
fontsize = 45 fontsize = 45
fontcolor = #ffffff fontcolor = #ffffff
x = 640 x = 640
y = 1000 y = 1000
text = '' ; text =

View file

@ -1,15 +1,13 @@
[default] [meta]
schedule = https://projects.alpaka.space/media/jhber19-schedule.xml schedule = https://projects.alpaka.space/media/jhber19-schedule.xml
template = intro-alpha.mov template = intro-alpha.mov
alpha = true alpha = true
prores = true prores = true
fontfile = true inout_type = n
inout = n
[title] [title]
in = 175 in = 175
out = 260 out = 260
fontfamily =
fontfile = SourceSansPro-Bold.otf fontfile = SourceSansPro-Bold.otf
fontsize = 90 fontsize = 90
fontcolor = #ffffff fontcolor = #ffffff
@ -19,7 +17,6 @@ y = 450
[speaker] [speaker]
in = 175 in = 175
out = 260 out = 260
fontfamily =
fontfile = SourceSansPro-Regular.otf fontfile = SourceSansPro-Regular.otf
fontsize = 36 fontsize = 36
fontcolor = #ffffff fontcolor = #ffffff
@ -29,11 +26,10 @@ y = 900
[text] [text]
in = 200 in = 200
out = 250 out = 250
fontfamily =
fontfile = SourceSansPro-Regular.otf fontfile = SourceSansPro-Regular.otf
fontsize = 45 fontsize = 45
fontcolor = #c68100 fontcolor = #c68100
x = (w-text_w)/2 x = (w-text_w)/2
y = 927 y = 927
text = '' ; text =

View file

@ -1,37 +1,35 @@
[default] [meta]
schedule = https://releasing.c3voc.de/releases/jhjue2020/schedule-jhjue20.xml schedule = https://releasing.c3voc.de/releases/jhjue2020/schedule-jhjue20.xml
; intro_template.ts was derived from a png which was derived from jhjue-20-intro.svg ; intro_template.ts was derived from a png which was derived from jhjue-20-intro.svg
; ffmpeg -loop 1 -i jh20-jue/jh20-jue-intro.png -ar 48000 -ac 2 -f s16le -i /dev/zero -c:v mpeg2video -pix_fmt:v yuv420p -qscale:v 2 -qmin:v 2 -qmax:v 7 -keyint_min 0 -bf 0 -g 0 -intra:0 -maxrate:0 90M -c:a mp2 -b:a 384k -t 5 jh20-jue/jh20-jue_intro_template.ts ; ffmpeg -loop 1 -i jh20-jue/jh20-jue-intro.png -ar 48000 -ac 2 -f s16le -i /dev/zero -c:v mpeg2video -pix_fmt:v yuv420p -qscale:v 2 -qmin:v 2 -qmax:v 7 -keyint_min 0 -bf 0 -g 0 -intra:0 -maxrate:0 90M -c:a mp2 -b:a 384k -t 5 jh20-jue/jh20-jue_intro_template.ts
template = jh20-jue_intro_template.ts template = jh20-jue_intro_template.ts
alpha = false alpha = false
prores = false prores = false
; enable using a font file
fontfile = true
; in and out time format: t for seconds, n for frame number ; in and out time format: t for seconds, n for frame number
inout = n inout = n
; fields for title and speaker names are empty in the template.ts, so we'll render them in via ffmpeg ;; Some font settings can have defaults, which can be overridden in the
;; 'title', 'speaker' and 'text' sections below.
[default]
;; default font
fontfile = ebisu.ttf
;; default font color
fontcolor = #ffffff
;; fields for title and speaker names are empty in the template.ts, so we'll render them in via ffmpeg
[title] [title]
; inframe for title ; inframe for title
in = 20 in = 20
; outframe for totle ; outframe for title
out = 225 out = 225
; title font (either font family or file, see default setting above)
fontfamily =
fontfile = ebisu.ttf
; title font size ; title font size
fontsize = 70 fontsize = 70
; title color
fontcolor = #ffffff
; title position from upper left corner
x = 600 x = 600
y = 865 y = 865
[speaker] [speaker]
in = 40 in = 40
out = 225 out = 225
fontfamily =
fontfile = ebisu.ttf
fontsize = 40 fontsize = 40
fontcolor = #eeeeee fontcolor = #eeeeee
x = 600 x = 600
@ -41,13 +39,10 @@ y = 950
[text] [text]
in = 3 in = 3
out = 4 out = 4
fontfamily =
fontfile = ebisu.ttf
fontsize = 45 fontsize = 45
fontcolor = #ffffff
x = 640 x = 640
y = 1000 y = 1000
text = '' ; text =
; build intros via ; build intros via

View file

@ -1,15 +1,13 @@
[default] [meta]
schedule = https://pretalx.c3voc.de/jugend-hackt-rhein-neckar-2021/schedule/export/schedule.xml schedule = https://pretalx.c3voc.de/jugend-hackt-rhein-neckar-2021/schedule/export/schedule.xml
template = jh21-rn-template.ts template = jh21-rn-template.ts
alpha = false alpha = false
prores = false prores = false
fontfile = true inout_type = t
inout = t
[title] [title]
in = 1 in = 1
out = 6.5 out = 6.5
fontfamily =
fontfile = SourceSansPro-Semibold.ttf fontfile = SourceSansPro-Semibold.ttf
fontsize = 85 fontsize = 85
fontcolor = #ffffff fontcolor = #ffffff
@ -19,7 +17,6 @@ y = 877
[speaker] [speaker]
in = 2 in = 2
out = 6.5 out = 6.5
fontfamily =
fontfile = SourceSansPro-Semibold.ttf fontfile = SourceSansPro-Semibold.ttf
fontsize = 45 fontsize = 45
fontcolor = #ffffff fontcolor = #ffffff
@ -29,11 +26,10 @@ y = 954
[text] [text]
in = 3 in = 3
out = 6.5 out = 6.5
fontfamily =
fontfile = SourceSansPro-Semibold.ttf fontfile = SourceSansPro-Semibold.ttf
fontsize = 45 fontsize = 45
fontcolor = #ffffff fontcolor = #ffffff
x = 1920 x = 1920
y = 1080 y = 1080
text = '' ; text =

View file

@ -1,39 +1,52 @@
[default] [meta]
schedule = https://pretalx.c3voc.de/jhhh23/schedule/export/schedule.xml schedule = https://pretalx.c3voc.de/jhhh23/schedule/export/schedule.xml
;; path to background video
template = intro-background.ts template = intro-background.ts
;; whether background video uses transparency (needs to be .mov)
alpha = false alpha = false
;; whether background video is prores 4444
prores = false prores = false
fontfile = true ;; in and out time format: t for seconds, n for frame number
inout = t inout_type = t
;; fade-in duration (seconds), leave out or set to zero to disable
;fade_duration = 0.5
;; Some font settings can have defaults, which can be overridden in the
;; 'title', 'speaker' and 'text' sections below.
[default]
;; default font
fontfile = SourceSansPro-Semibold.ttf
;; default font color
fontcolor = #ffffff
;; fields for title and speaker names are empty in the template.ts, so we'll render them in via ffmpeg
;; parameters are:
;; - in: start frame/ time
;; - out: end frame/ time
;; - fontfile: font file
;; - fontcolor: font color
;; - fontsize: font size (pixel)
;; - x: horizontal position (top left corner)
;; - y: vertical position (top left corner)
[title] [title]
in = 1 in = 1
out = 9.5 out = 9.5
fontfamily =
fontfile = SourceSansPro-Semibold.ttf
fontsize = 67 fontsize = 67
fontcolor = #ffffff
x = 400 x = 400
y = 870 y = 870
[speaker] [speaker]
in = 2 in = 2
out = 9 out = 9
fontfamily =
fontfile = SourceSansPro-Semibold.ttf
fontsize = 50 fontsize = 50
fontcolor = #ffffff
x = 400 x = 400
y = 950 y = 950
;; optional extra text, comment out "text" field to disable
[text] [text]
in = 0 in = 0
out = 0 out = 0
fontfamily =
fontfile = SourceSansPro-Semibold.ttf
fontsize = 0 fontsize = 0
fontcolor = #ffffff
x = 0 x = 0
y = 0 y = 0
text = '' ;text = some additional text

View file

@ -1,327 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# vim: tabstop=4 shiftwidth=4 expandtab # vim: tabstop=4 shiftwidth=4 expandtab
import os print("ERROR: The functionality of this script was added to 'make-ffmpeg.py'!")
import sys print("Specify meta.fade_duration = 0.5 in the config.ini for the same effect.")
import subprocess
import renderlib
import schedulelib
import argparse
import shlex
from PIL import ImageFont
from configparser import ConfigParser
import json
# Parse arguments
parser = argparse.ArgumentParser(
description='C3VOC Intro-Outro-Generator - Variant which renders only using video filters in ffmpeg',
usage="./make-ffmpeg.py yourproject/",
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('project', action="store", metavar='Project folder', type=str, help='''
Path to your project folder
''')
parser.add_argument('--debug', action="store_true", default=False, help='''
Run script in debug mode and render with placeholder texts,
not parsing or accessing a schedule.
This argument must not be used together with --id
Usage: ./make-ffmpeg.py yourproject/ --debug
''')
parser.add_argument('--id', dest='ids', nargs='+', action="store", type=int, help='''
Only render the given ID(s) from your projects schedule.
This argument must not be used together with --debug
Usage: ./make-adobe-after-effects.py yourproject/ --id 4711 0815 4223 1337
''')
parser.add_argument('--room', dest='rooms', nargs='+', action="store", type=str, help='''
Only render the given room(s) from your projects schedule.
This argument must not be used together with --debug
Usage: ./make-adobe-after-effects.py yourproject/ --room "HfG_Studio" "ZKM_Vortragssaal"
''')
parser.add_argument('--skip', nargs='+', action="store", type=str, help='''
Skip ID(s) not needed to be rendered.
Usage: ./make-ffmpeg.py yourproject/ --skip 4711 0815 4223 1337
''')
parser.add_argument('--force', action="store_true", default=False, help='''
Force render if file exists.
''')
args = parser.parse_args()
if (args.skip is None):
args.skip = []
def headline(str):
print("##################################################")
print(str)
print("##################################################")
print()
def error(str):
headline(str)
parser.print_help()
sys.exit(1)
cparser = ConfigParser()
cparser.read(os.path.join(os.path.dirname(args.project), 'config.ini'))
template = cparser['default']['template']
alpha = cparser['default']['alpha']
prores = cparser['default']['prores']
fade_duration = 0.5
title_in = float(cparser['title']['in'])
title_out = float(cparser['title']['out'])
title_duration = title_out - title_in
title_font = cparser['title']['font']
title_fontsize = int(cparser['title']['fontsize'])
title_fontcolor = cparser['title']['fontcolor']
title_x = int(cparser['title']['x'])
title_y = int(cparser['title']['y'])
speaker_in = float(cparser['speaker']['in'])
speaker_out = float(cparser['speaker']['out'])
speaker_duration = speaker_out - speaker_in
speaker_font = cparser['speaker']['font']
speaker_fontsize = int(cparser['speaker']['fontsize'])
speaker_fontcolor = cparser['speaker']['fontcolor']
speaker_x = int(cparser['speaker']['x'])
speaker_y = int(cparser['speaker']['y'])
text_in = float(cparser['text']['in'])
text_out = float(cparser['text']['out'])
text_duration = text_out - text_in
text_font = cparser['text']['font']
text_fontsize = int(cparser['text']['fontsize'])
text_fontcolor = cparser['text']['fontcolor']
text_x = int(cparser['text']['x'])
text_y = int(cparser['text']['y'])
text_text = cparser['text']['text']
font_t = os.path.join(os.path.dirname(args.project), title_font)
font_s = os.path.join(os.path.dirname(args.project), speaker_font)
font_tt = os.path.join(os.path.dirname(args.project), text_font)
fileformat = os.path.splitext(template)[1]
infile = os.path.join(os.path.dirname(args.project), template)
schedule = cparser['default']['schedule']
if not (os.path.exists(os.path.join(args.project, template))):
error("Template file {} in Project Path is missing".format(template))
for ffile in (title_font, speaker_font, text_font):
if not (os.path.exists(os.path.join(args.project, ffile))):
error("Font file {} in Project Path is missing".format(ffile))
if not (os.path.exists(os.path.join(args.project, 'config.ini'))):
error("config.ini file in Project Path is missing")
if alpha == 'true' and not fileformat == '.mov':
error("Alpha can only be rendered with .mov source files")
if not args.project:
error("The Project Path is a required argument")
if not args.debug and not schedule:
error("Either specify --debug or supply a schedule in config.ini")
if args.debug:
persons = ['Thomas Roth', 'Dmitry Nedospasov', 'Josh Datko']
events = [{
'id': 'debug',
'title': 'wallet.fail',
'subtitle': 'Hacking the most popular cryptocurrency hardware wallets',
'persons': persons,
'personnames': ', '.join(persons),
'room': 'Borg',
}]
else:
events = list(schedulelib.events(schedule))
def describe_event(event):
return "#{}: {}".format(event['id'], event['title'])
def event_print(event, message):
print("{} {}".format(describe_event(event), message))
def fmt_command(command, **kwargs):
args = {}
for key, value in kwargs.items():
args[key] = shlex.quote(value)
command = command.format(**args)
return shlex.split(command)
def run(command, **kwargs):
return subprocess.check_call(
fmt_command(command, **kwargs),
stderr=subprocess.STDOUT,
stdout=subprocess.DEVNULL)
def fit_text(string: str, frame_width):
split_line = [x.strip() for x in string.split()]
lines = []
w = 0
line_num = 0
line = []
for word in split_line:
new_line = line + [word.rstrip(':')]
w, _ = translation_font.getsize(" ".join(new_line))
print(w, new_line)
if w > frame_width:
print("too wide, breaking", line)
lines.append(' '.join(line))
line = []
line.append(word.rstrip(':'))
if word.endswith(':'):
print(':, breaking', line)
lines.append(' '.join(line))
line = []
if line:
lines.append(' '.join(line))
return lines
def fit_title(string: str):
global translation_font
translation_font = ImageFont.truetype(font_t, size=title_fontsize-10, encoding="unic")
title = fit_text(string, 1080)
return title
def fit_speaker(string: str):
global translation_font
translation_font = ImageFont.truetype(font_s, size=speaker_fontsize-10, encoding="unic")
speaker = fit_text(string, 1080)
return speaker
def enqueue_job(event):
event_id = str(event['id'])
if event_id in args.skip:
event_print(event, "skipping " + str(event['id']))
return
if (os.path.exists(os.path.join(args.project, event_id + '.ts')) or os.path.exists(os.path.join(args.project, event_id + '.mov'))) and not args.force:
event_print(event, "file exist, skipping " + str(event['id']))
return
event_title = str(event['title'])
event_personnames = str(event['personnames'])
event_title = event_title.replace('"', '')
event_title = event_title.replace('\'', '')
event_personnames = event_personnames.replace('"', '')
t = fit_title(event_title)
s = fit_speaker(event_personnames)
print(s)
if args.debug:
print('Title: ', t)
print('Speaker: ', s)
outfile = os.path.join(os.path.dirname(args.project), event_id + '.ts')
videofilter = ""
for idx, line in enumerate(t):
videofilter += "drawtext=fontfile={fontfile}:fontsize={fontsize}:fontcolor={fontcolor}:x={x}:y={y}:text='{text}':".format(
fontfile=font_t,
fontsize=title_fontsize,
fontcolor=title_fontcolor,
x=title_x,
y=title_y + (idx * title_fontsize),
text=line)
videofilter += "alpha='if(lt(t,{fade_in_start_time}),0,if(lt(t,{fade_in_end_time}),(t-{fade_in_start_time})/{fade_duration},if(lt(t,{fade_out_start_time}),1,if(lt(t,{fade_out_end_time}),({fade_duration}-(t-{fade_out_start_time}))/{fade_duration},0))))',".format(
fade_in_start_time=title_in,
fade_in_end_time=title_in + fade_duration,
fade_out_start_time=title_in + fade_duration + title_duration,
fade_out_end_time=title_in + fade_duration + title_duration + fade_duration,
fade_duration=fade_duration
)
for idx, line in enumerate(s):
videofilter += "drawtext=fontfile={fontfile}:fontsize={fontsize}:fontcolor={fontcolor}:x={x}:y={y}:text='{text}':".format(
fontfile=font_s,
fontsize=speaker_fontsize,
fontcolor=speaker_fontcolor,
x=speaker_x,
y=speaker_y + (idx * speaker_fontsize),
text=line)
videofilter += "alpha='if(lt(t,{fade_in_start_time}),0,if(lt(t,{fade_in_end_time}),(t-{fade_in_start_time})/{fade_duration},if(lt(t,{fade_out_start_time}),1,if(lt(t,{fade_out_end_time}),({fade_duration}-(t-{fade_out_start_time}))/{fade_duration},0))))',".format(
fade_in_start_time=speaker_in,
fade_in_end_time=speaker_in + fade_duration,
fade_out_start_time=speaker_in + fade_duration + speaker_duration,
fade_out_end_time=speaker_in + fade_duration + speaker_duration + fade_duration,
fade_duration=fade_duration
)
if fileformat == '.mov':
if alpha == 'true':
if prores == 'true':
cmd = 'ffmpeg -y -i "{0}" -vf "{1}" -vcodec prores_ks -pix_fmt yuva444p10le -profile:v 4444 -shortest -movflags faststart -f mov "{2}"'.format(
infile, videofilter, outfile)
else:
cmd = 'ffmpeg -y -i "{0}" -vf "{1}" -shortest -c:v qtrle -movflags faststart -f mov "{2}"'.format(
infile, videofilter, outfile)
else:
cmd = 'ffmpeg -y -i "{0}" -vf "{1}" -map 0:0 -c:v mpeg2video -q:v 2 -aspect 16:9 -map 0:1 -c:a mp2 -b:a 384k -shortest -f mpegts "{2}"'.format(
infile, videofilter, outfile)
else:
cmd = 'ffmpeg -y -i "{0}" -vf "{1}" -map 0:0 -c:v mpeg2video -pix_fmt:v yuv420p -qscale:v 2 -qmin:v 2 -qmax:v 7 -keyint_min 0 -bf 0 -g 0 -maxrate:0 90M -aspect 16:9 -map 0:1 -c:a mp2 -b:a 384k -shortest -f mpegts "{2}"'.format(
infile, videofilter, outfile)
if args.debug:
print(cmd)
run(cmd)
return event_id
if args.ids:
if len(args.ids) == 1:
print("enqueuing {} job".format(len(args.ids)))
else:
print("enqueuing {} jobs".format(len(args.ids)))
else:
if len(events) == 1:
print("enqueuing {} job".format(len(events)))
else:
print("enqueuing {} jobs".format(len(events)))
for event in events:
if args.ids and event['id'] not in args.ids:
continue
if args.rooms and event['room'] not in args.rooms:
print("skipping room %s (%s)" % (event['room'], event['title']))
continue
event_print(event, "enqueued as " + str(event['id']))
job_id = enqueue_job(event)
if not job_id:
event_print(event, "job was not enqueued successfully, skipping postprocessing")
continue
print('all done')

View file

@ -1,153 +1,165 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# vim: tabstop=4 shiftwidth=4 expandtab # vim: tabstop=4 shiftwidth=4 expandtab
"""See jugendhackt/config.ini for some config file documentation."""
import os import os
import sys import sys
import subprocess import subprocess
import schedulelib
import argparse import argparse
import shlex
from PIL import ImageFont
from configparser import ConfigParser
import json
import platform
import ssl import ssl
from configparser import ConfigParser
from pathlib import PurePath
import platform
from PIL import ImageFont
import schedulelib
ssl._create_default_https_context = ssl._create_unverified_context ssl._create_default_https_context = ssl._create_unverified_context
# Parse arguments FRAME_WIDTH = 1920
parser = argparse.ArgumentParser(
description='C3VOC Intro-Outro-Generator - Variant which renders only using video filters in ffmpeg',
usage="./make-ffmpeg.py yourproject/",
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('project', action="store", metavar='Project folder', type=str, help='''
Path to your project folder
''')
parser.add_argument('--debug', action="store_true", default=False, help='''
Run script in debug mode and render with placeholder texts,
not parsing or accessing a schedule.
This argument must not be used together with --id
Usage: ./make-ffmpeg.py yourproject/ --debug
''')
parser.add_argument('--id', dest='ids', nargs='+', action="store", type=int, help='''
Only render the given ID(s) from your projects schedule.
This argument must not be used together with --debug
Usage: ./make-adobe-after-effects.py yourproject/ --id 4711 0815 4223 1337
''')
parser.add_argument('--room', dest='rooms', nargs='+', action="store", type=str, help='''
Only render the given room(s) from your projects schedule.
This argument must not be used together with --debug
Usage: ./make-adobe-after-effects.py yourproject/ --room "HfG_Studio" "ZKM_Vortragssaal"
''')
parser.add_argument('--skip', nargs='+', action="store", type=str, help='''
Skip ID(s) not needed to be rendered.
Usage: ./make-ffmpeg.py yourproject/ --skip 4711 0815 4223 1337
''')
parser.add_argument('--force', action="store_true", default=False, help='''
Force render if file exists.
''')
args = parser.parse_args()
if (args.skip is None):
args.skip = []
def headline(str): class TextConfig:
print("##################################################") inpoint: float
print(str) outpoint: float
print("##################################################") x: int
print() y: int
fontfile_path: str
fontsize: int
fontcolor: str
bordercolor: str = None # border is added, if a color is set
def uses_fontfile(self):
return self.fontfile_path is not None
def parse(self, cparser_sect, default_fontfile, default_fontcolor):
self.inpoint = cparser_sect.getfloat('in')
self.outpoint = cparser_sect.getfloat('out')
self.x = cparser_sect.getint('x')
self.y = cparser_sect.getint('y')
self.fontcolor = cparser_sect.get('fontcolor', default_fontcolor)
fontfile = cparser_sect.get('fontfile', default_fontfile)
self.fontfile_path = str(PurePath(args.project, fontfile).as_posix())
if not os.path.exists(self.fontfile_path):
error("Font file {} in Project Path is missing".format(self.fontfile_path))
self.fontsize = cparser_sect.getint('fontsize')
self.bordercolor = cparser_sect.get('bordercolor', None)
def fit_text(self, text: str) -> list[str]:
if not text:
return [""]
font = ImageFont.truetype(
self.fontfile_path, size=self.fontsize, encoding="unic")
return fit_text(text, (FRAME_WIDTH-self.x-100), font)
def get_ffmpeg_filter(self, inout_type: str, fade_time: float, text: list[str]):
if not text:
return ""
text_duration = self.outpoint - self.inpoint
filter_str = ""
for idx, line in enumerate(text):
filter_str += "drawtext=enable='between({},{},{})':x={}:y={}".format(
inout_type, self.inpoint, self.outpoint, self.x, self.y + (idx*self.fontsize))
filter_str += ":fontfile='{}':fontsize={}:fontcolor={}:text={}".format(
self.fontfile_path, self.fontsize, self.fontcolor, ffmpeg_escape_str(line))
if self.bordercolor is not None:
filter_str += ":borderw={}:bordercolor={}".format(
self.fontsize / 30, self.bordercolor)
if fade_time > 0:
filter_str += ":alpha='if(lt(t,{fade_in_start_time}),0,if(lt(t,{fade_in_end_time}),(t-{fade_in_start_time})/{fade_duration},if(lt(t,{fade_out_start_time}),1,if(lt(t,{fade_out_end_time}),({fade_duration}-(t-{fade_out_start_time}))/{fade_duration},0))))'".format(
fade_in_start_time=self.inpoint,
fade_in_end_time=self.inpoint + fade_time,
fade_out_start_time=self.inpoint + fade_time + text_duration,
fade_out_end_time=self.inpoint + fade_time + text_duration + fade_time,
fade_duration=fade_time)
filter_str += ","
return filter_str[:-1]
def error(str): class Config:
headline(str) schedule: str
parser.print_help() template_file: str # video background
sys.exit(1) alpha: bool = False
prores: bool = False
inout_type: str = "t" # in and out time format: t for seconds, n for frame number
fade_duration: float = 0 # fade duration in seconds, 0 to disable
fileext: str
title: TextConfig
speaker: TextConfig
text: TextConfig
extra_text: str = "" # additional text
cparser = ConfigParser() def parse_config(filename) -> Config:
cparser.read(os.path.join(os.path.dirname(args.project), 'config.ini')) if not os.path.exists(filename):
template = cparser['default']['template']
alpha = cparser['default']['alpha']
prores = cparser['default']['prores']
fontfile = cparser['default']['fontfile'] # use a font file instead of a font family
inout = cparser['default']['inout'] # in and out time format: t for seconds, n for frame number
title_in = cparser['title']['in']
title_out = cparser['title']['out']
title_fontfamily = cparser['title']['fontfamily']
title_fontfile = cparser['title']['fontfile']
title_fontsize = cparser['title']['fontsize']
title_fontcolor = cparser['title']['fontcolor']
title_x = cparser['title']['x']
title_y = cparser['title']['y']
speaker_in = cparser['speaker']['in']
speaker_out = cparser['speaker']['out']
speaker_fontfamily = cparser['speaker']['fontfamily']
speaker_fontfile = cparser['speaker']['fontfile']
speaker_fontsize = cparser['speaker']['fontsize']
speaker_fontcolor = cparser['speaker']['fontcolor']
speaker_x = cparser['speaker']['x']
speaker_y = cparser['speaker']['y']
text_in = cparser['text']['in']
text_out = cparser['text']['out']
text_fontfamily = cparser['text']['fontfamily']
text_fontfile = cparser['text']['fontfile']
text_fontsize = cparser['text']['fontsize']
text_fontcolor = cparser['text']['fontcolor']
text_x = cparser['text']['x']
text_y = cparser['text']['y']
text_text = cparser['text']['text']
font_t = os.path.join(os.path.dirname(args.project), title_fontfile)
font_s = os.path.join(os.path.dirname(args.project), speaker_fontfile)
font_tt = os.path.join(os.path.dirname(args.project), text_fontfile)
fileformat = os.path.splitext(template)[1]
infile = os.path.join(os.path.dirname(args.project), template)
schedule = cparser['default']['schedule']
if not (os.path.exists(os.path.join(args.project, template))):
error("Template file {} in Project Path is missing".format(template))
for ffile in (title_fontfile, speaker_fontfile, text_fontfile):
if not (os.path.exists(os.path.join(args.project, ffile))):
error("Font file {} in Project Path is missing".format(ffile))
if not (os.path.exists(os.path.join(args.project, 'config.ini'))):
error("config.ini file in Project Path is missing") error("config.ini file in Project Path is missing")
if alpha == 'true' and not fileformat == '.mov': conf = Config()
cparser = ConfigParser()
cparser.read(filename)
meta = cparser['meta']
conf.schedule = meta.get('schedule')
infile = PurePath(args.project, meta.get('template'))
conf.template_file = str(infile)
conf.alpha = meta.getboolean('alpha', conf.alpha)
conf.prores = meta.getboolean('prores', conf.prores)
conf.inout_type = meta.get('inout_type', conf.inout_type)
conf.fade_duration = meta.getfloat('fade_duration', conf.fade_duration)
defaults = cparser['default']
default_fontfile = defaults.get('fontfile', None)
default_fontcolor = defaults.get('fontcolor', "#ffffff")
conf.title = TextConfig()
conf.title.parse(cparser['title'], default_fontfile, default_fontcolor)
conf.speaker = TextConfig()
conf.speaker.parse(cparser['speaker'], default_fontfile, default_fontcolor)
conf.text = TextConfig()
conf.text.parse(cparser['text'], default_fontfile, default_fontcolor)
conf.extra_text = cparser['text'].get('text', '')
conf.fileext = infile.suffix
if not os.path.exists(conf.template_file):
error("Template file {} in Project Path is missing".format(conf.template_file))
if conf.alpha and conf.fileext != '.mov':
error("Alpha can only be rendered with .mov source files") error("Alpha can only be rendered with .mov source files")
if not args.project: if not args.project:
error("The Project Path is a required argument") error("The Project Path is a required argument")
if not args.debug and not schedule: if not args.debug and not conf.schedule:
error("Either specify --debug or supply a schedule in config.ini") error("Either specify --debug or supply a schedule in config.ini")
if args.debug: return conf
persons = ['Thomas Roth', 'Dmitry Nedospasov', 'Josh Datko',]
events = [{
'id': 'debug',
'title': 'wallet.fail',
'subtitle': 'Hacking the most popular cryptocurrency hardware wallets',
'persons': persons,
'personnames': ', '.join(persons),
'room': 'Borg',
}]
else:
events = list(schedulelib.events(schedule)) def error(err_str):
print("##################################################")
print(err_str)
print("##################################################")
print()
parser.print_help()
sys.exit(1)
def describe_event(event): def describe_event(event):
@ -158,151 +170,172 @@ def event_print(event, message):
print("{} {}".format(describe_event(event), message)) print("{} {}".format(describe_event(event), message))
def fmt_command(command, **kwargs): def fit_text(string: str, max_width: int, font: ImageFont) -> list[str]:
args = {} """Break text into array of strings which fit certain a width (in pixels) for the specified font."""
for key, value in kwargs.items():
args[key] = shlex.quote(value)
command = command.format(**args)
return shlex.split(command)
def run(command, **kwargs):
return subprocess.check_call(
fmt_command(command, **kwargs),
stderr=subprocess.STDOUT,
stdout=subprocess.DEVNULL)
def fit_text(string: str, frame_width):
split_line = [x.strip() for x in string.split()] split_line = [x.strip() for x in string.split()]
lines = "" lines = []
w = 0 w = 0
line_num = 0 line = []
line = ""
for word in split_line: for word in split_line:
left, top, right, bottom = translation_font.getbbox(" ".join([line, word])) new_line = line + [word.rstrip(':')]
width, height = right - left, bottom - top w = font.getlength(" ".join(new_line))
if width > (frame_width - (2 * 6)): if w > max_width:
lines += line.strip() + "\n" lines.append(' '.join(line))
line = "" line = []
line += word + " " line.append(word.rstrip(':'))
if word.endswith(':'):
lines.append(' '.join(line))
line = []
if line:
lines.append(' '.join(line))
lines += line.strip()
return lines return lines
def fit_title(string: str, fontsize: int, x_offset: int): def ffmpeg_escape_str(text: str) -> str:
global translation_font # Escape according to https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
translation_font = ImageFont.truetype( # and don't put the string in quotes afterwards!
font_t, size=fontsize, encoding="unic") text = text.replace(",", r"\,")
title = fit_text(string, (1920-x_offset-100)) text = text.replace(':', r"\\:")
text = text.replace("'", r"\\\'")
return title return text
def fit_speaker(string: str, fontsize: int, x_offset: int): def enqueue_job(conf: Config, event):
global translation_font
translation_font = ImageFont.truetype(
font_s, size=fontsize, encoding="unic")
speaker = fit_text(string, (1920-x_offset-100))
return speaker
def enqueue_job(event):
event_id = str(event['id']) event_id = str(event['id'])
outfile = str(PurePath(args.project, event_id + '.ts'))
outfile_mov = str(PurePath(args.project, event_id + '.mov'))
if event_id in args.skip: if event_id in args.skip:
event_print(event, "skipping " + str(event['id'])) event_print(event, "skipping " + str(event['id']))
return return
if (os.path.exists(os.path.join(args.project, event_id + '.ts')) or os.path.exists(os.path.join(args.project, event_id + '.mov'))) and not args.force: if (os.path.exists(outfile) or os.path.exists(outfile_mov)) and not args.force:
event_print(event, "file exist, skipping " + str(event['id'])) event_print(event, "file exist, skipping " + str(event['id']))
return return
event_title = str(event['title']) event_title = str(event['title'])
event_personnames = str(event['personnames']) event_personnames = str(event['personnames'])
event_title = event_title.replace('"', '\\"')
event_title = event_title.replace('\'', '')
event_personnames = event_personnames.replace('"', '\\"')
t = fit_title(event_title, int(title_fontsize), int(title_x)) title = conf.title.fit_text(event_title)
t = t.replace(':', "\:") # the ffmpeg command needs colons to be escaped speakers = conf.speaker.fit_text(event_personnames)
s = fit_speaker(event_personnames, int(speaker_fontsize), int(speaker_x)) extra_text = conf.text.fit_text(conf.extra_text)
if args.debug: if args.debug:
print('Title: ', t) print('Title: ', title)
print('Speaker: ', s) print('Speaker: ', speakers)
outfile = os.path.join(os.path.dirname(args.project), event_id + '.ts')
if platform.system() == 'Windows': if platform.system() == 'Windows':
ffmpeg_path = './ffmpeg.exe' ffmpeg_path = './ffmpeg.exe'
font_t_win = "/".join(font_t.split("\\"))
font_s_win = "/".join(font_s.split("\\"))
font_tt_win = "/".join(font_tt.split("\\"))
else: else:
ffmpeg_path = 'ffmpeg' ffmpeg_path = 'ffmpeg'
if fontfile == 'true': videofilter = conf.title.get_ffmpeg_filter(conf.inout_type, conf.fade_duration, title) + ","
if platform.system() == 'Windows': videofilter += conf.speaker.get_ffmpeg_filter(conf.inout_type,
videofilter = "drawtext=enable='between({8},{0},{1})':fontfile='{2}':fontsize={3}:fontcolor={4}:x={5}:y={6}:text='{7}',".format( conf.fade_duration, speakers) + ","
title_in, title_out, font_t_win, title_fontsize, title_fontcolor, title_x, title_y, t, inout) videofilter += conf.text.get_ffmpeg_filter(conf.inout_type, conf.fade_duration, extra_text)
videofilter += "drawtext=enable='between({8},{0},{1})':fontfile='{2}':fontsize={3}:fontcolor={4}:x={5}:y={6}:text='{7}',".format(
speaker_in, speaker_out, font_s_win, speaker_fontsize, speaker_fontcolor, speaker_x, speaker_y, s, inout)
videofilter += "drawtext=enable='between({8},{0},{1})':fontfile='{2}':fontsize={3}:fontcolor={4}:x={5}:y={6}:text='{7}'".format(
text_in, text_out, font_tt_win, text_fontsize, text_fontcolor, text_x, text_y, text_text, inout)
else:
videofilter = "drawtext=enable='between({8},{0},{1})':fontfile='{2}':fontsize={3}:fontcolor={4}:x={5}:y={6}:text='{7}',".format(
title_in, title_out, font_t, title_fontsize, title_fontcolor, title_x, title_y, t, inout)
videofilter += "drawtext=enable='between({8},{0},{1})':fontfile='{2}':fontsize={3}:fontcolor={4}:x={5}:y={6}:text='{7}',".format(
speaker_in, speaker_out, font_s, speaker_fontsize, speaker_fontcolor, speaker_x, speaker_y, s, inout)
videofilter += "drawtext=enable='between({8},{0},{1})':fontfile='{2}':fontsize={3}:fontcolor={4}:x={5}:y={6}:text='{7}'".format(
text_in, text_out, font_tt, text_fontsize, text_fontcolor, text_x, text_y, text_text, inout)
else:
videofilter = "drawtext=enable='between({8},{0},{1})':font='{2}':fontsize={3}:fontcolor={4}:x={5}:y={6}:text='{7}',".format(
title_in, title_out, title_fontfamily, title_fontsize, title_fontcolor, title_x, title_y, t, inout)
videofilter += "drawtext=enable='between({8},{0},{1})':font='{2}':fontsize={3}:fontcolor={4}:x={5}:y={6}:text='{7}',".format(
speaker_in, speaker_out, speaker_fontfamily, speaker_fontsize, speaker_fontcolor, speaker_x, speaker_y, s, inout)
videofilter += "drawtext=enable='between({8},{0},{1})':font='{2}':fontsize={3}:fontcolor={4}:x={5}:y={6}:text='{7}'".format(
text_in, text_out, text_fontfamily, text_fontsize, text_fontcolor, text_x, text_y, text_text, inout)
if fileformat == '.mov': cmd = [ffmpeg_path, '-y', '-i', conf.template_file, '-vf', videofilter]
if alpha == 'true':
if prores == 'true': if conf.fileext == '.mov' and conf.alpha:
cmd = '{3} -y -i "{0}" -vf "{1}" -vcodec prores_ks -pix_fmt yuva444p10le -profile:v 4444 -shortest -movflags faststart -f mov "{2}"'.format( if conf.prores:
infile, videofilter, outfile, ffmpeg_path) cmd += ['-vcodec', 'prores_ks', '-pix_fmt', 'yuva444p10le', '-profile:v',
'4444', '-shortest', '-movflags', 'faststart', '-f', 'mov', outfile_mov]
else: else:
cmd = '{3} -y -i "{0}" -vf "{1}" -shortest -c:v qtrle -movflags faststart -f mov "{2}"'.format( cmd += ['-shortest', '-c:v', 'qtrle', '-movflags',
infile, videofilter, outfile, ffmpeg_path) 'faststart', '-f', 'mov', outfile_mov]
else: else:
cmd = '{3} -y -i "{0}" -vf "{1}" -map 0:0 -c:v mpeg2video -q:v 2 -aspect 16:9 -map 0:1 -c:a mp2 -b:a 384k -shortest -f mpegts "{2}"'.format( cmd += ['-map', '0:0', '-c:v', 'mpeg2video', '-q:v', '2', '-aspect', '16:9', '-map',
infile, videofilter, outfile, ffmpeg_path) '0:1', '-c:a', 'mp2', '-b:a', '384k', '-shortest', '-f', 'mpegts', outfile]
else:
cmd = '{3} -y -i "{0}" -vf "{1}" -map 0:0 -c:v mpeg2video -q:v 2 -aspect 16:9 -map 0:1 -c:a mp2 -b:a 384k -shortest -f mpegts "{2}"'.format(
infile, videofilter, outfile, ffmpeg_path)
if args.debug: if args.debug:
print(cmd) print(cmd)
run(cmd) subprocess.check_call(cmd,
stderr=subprocess.STDOUT,
stdout=subprocess.DEVNULL
)
return event_id return event_id
if args.ids: if __name__ == "__main__":
# Parse arguments
parser = argparse.ArgumentParser(
description='C3VOC Intro-Outro-Generator - Variant which renders only using video filters in ffmpeg',
usage="./make-ffmpeg.py yourproject/",
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('project', action="store", metavar='Project folder', type=str, help='''
Path to your project folder
''')
parser.add_argument('--debug', action="store_true", default=False, help='''
Run script in debug mode and render with placeholder texts,
not parsing or accessing a schedule.
This argument must not be used together with --id
Usage: ./make-ffmpeg.py yourproject/ --debug
''')
parser.add_argument('--id', dest='ids', nargs='+', action="store", type=int, help='''
Only render the given ID(s) from your projects schedule.
This argument must not be used together with --debug
Usage: ./make-adobe-after-effects.py yourproject/ --id 4711 0815 4223 1337
''')
parser.add_argument('--room', dest='rooms', nargs='+', action="store", type=str, help='''
Only render the given room(s) from your projects schedule.
This argument must not be used together with --debug
Usage: ./make-adobe-after-effects.py yourproject/ --room "HfG_Studio" "ZKM_Vortragssaal"
''')
parser.add_argument('--skip', nargs='+', action="store", type=str, help='''
Skip ID(s) not needed to be rendered.
Usage: ./make-ffmpeg.py yourproject/ --skip 4711 0815 4223 1337
''')
parser.add_argument('--force', action="store_true", default=False, help='''
Force render if file exists.
''')
args = parser.parse_args()
if (args.skip is None):
args.skip = []
config = parse_config(PurePath(args.project, 'config.ini'))
if args.debug:
persons = ['Thomas Roth', 'Dmitry Nedospasov', 'Josh Datko',]
events = [{
'id': 'debug',
'title': 'wallet.fail and the longest talk title to test if the template is big enough',
'subtitle': 'Hacking the most popular cryptocurrency hardware wallets',
'persons': persons,
'personnames': ', '.join(persons),
'room': 'Borg',
}]
else:
events = list(schedulelib.events(config.schedule))
if args.ids:
if len(args.ids) == 1: if len(args.ids) == 1:
print("enqueuing {} job".format(len(args.ids))) print("enqueuing {} job".format(len(args.ids)))
else: else:
print("enqueuing {} jobs".format(len(args.ids))) print("enqueuing {} jobs".format(len(args.ids)))
else: else:
if len(events) == 1: if len(events) == 1:
print("enqueuing {} job".format(len(events))) print("enqueuing {} job".format(len(events)))
else: else:
print("enqueuing {} jobs".format(len(events))) print("enqueuing {} jobs".format(len(events)))
for event in events:
for event in events:
if args.ids and event['id'] not in args.ids: if args.ids and event['id'] not in args.ids:
continue continue
@ -312,10 +345,9 @@ for event in events:
event_print(event, "enqueued as " + str(event['id'])) event_print(event, "enqueued as " + str(event['id']))
job_id = enqueue_job(event) job_id = enqueue_job(config, event)
if not job_id: if not job_id:
event_print(event, "job was not enqueued successfully, skipping postprocessing") event_print(event, "job was not enqueued successfully, skipping postprocessing")
continue continue
print('all done')
print('all done')

View file

@ -1,16 +1,14 @@
[default] [meta]
#schedule = https://talks.mrmcd.net/2019/schedule/export/schedule.xml #schedule = https://talks.mrmcd.net/2019/schedule/export/schedule.xml
schedule = file:///home/thorti/git/c3voc/intro-outro-generator/mrmcd2019/schedule.xml schedule = file:///home/thorti/git/c3voc/intro-outro-generator/mrmcd2019/schedule.xml
template = mrmcd2019.mov template = mrmcd2019.mov
alpha = false alpha = false
prores = false prores = false
fontfile = true inout_type = n
inout = n
[title] [title]
in = 50 in = 50
out = 225 out = 225
fontfamily =
fontfile = Jura-Bold.ttf fontfile = Jura-Bold.ttf
fontsize = 80 fontsize = 80
fontcolor = #47acda fontcolor = #47acda
@ -20,7 +18,6 @@ y = 540
[speaker] [speaker]
in = 75 in = 75
out = 225 out = 225
fontfamily =
fontfile = Jura-Regular.ttf fontfile = Jura-Regular.ttf
fontsize = 50 fontsize = 50
fontcolor = #094762 fontcolor = #094762
@ -30,10 +27,9 @@ y = 950
[text] [text]
in = 242 in = 242
out = 324 out = 324
fontfamily =
fontfile = Jura-Regular.ttf fontfile = Jura-Regular.ttf
fontsize = 45 fontsize = 45
fontcolor = #c68100 fontcolor = #c68100
x = (w-text_w)/2 x = (w-text_w)/2
y = 927 y = 927
text = '' ; text =

View file

@ -1,5 +1,5 @@
pillow pillow>=8.0.0
cssutils==1.0.2 cssutils==1.0.2
lxml==4.9.1 lxml~=5.3
svg.path==4.0.2 svg.path~=6.0
Wand==0.6.5 Wand~=0.6.5

View file

@ -1,16 +1,16 @@
[default] [meta]
schedule = http://vcfb.de/2024/schedule.xml schedule = http://vcfb.de/2024/schedule.xml
# ffmpeg -loop 1 -i intro.png -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 -c:v libx264 -tune stillimage -pix_fmt yuv420p -c:a aac -r 25 -t 10 intro.mp4 # ffmpeg -loop 1 -i intro.png -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 -c:v libx264 -tune stillimage -pix_fmt yuv420p -c:a aac -r 25 -t 10 intro.mp4
template = intro.mp4 template = intro.mp4
alpha = false alpha = false
prores = false prores = false
fontfile = true inout_type = t
inout = t fade_duration = 0.5
[title] [title]
in = 1 in = 1
out = 9.5 out = 9.5
font = Computerfont.ttf fontfile = Computerfont.ttf
fontsize = 100 fontsize = 100
fontcolor = #ffffff fontcolor = #ffffff
x = 85 x = 85
@ -19,7 +19,7 @@ y = 122
[speaker] [speaker]
in = 2 in = 2
out = 9 out = 9
font = SourceSansPro-Semibold.ttf fontfile = SourceSansPro-Semibold.ttf
fontsize = 65 fontsize = 65
fontcolor = #ffffff fontcolor = #ffffff
x = 85 x = 85
@ -29,9 +29,9 @@ y = 861
[text] [text]
in = 0 in = 0
out = 0 out = 0
font = Computerfont.ttf fontfile = Computerfont.ttf
fontsize = 0 fontsize = 0
fontcolor = #ffffff fontcolor = #ffffff
x = 0 x = 0
y = 0 y = 0
text = '' ; text =