diff --git a/camp2023/config.ini b/camp2023/config.ini
index 2072250..e8c7ab6 100644
--- a/camp2023/config.ini
+++ b/camp2023/config.ini
@@ -1,15 +1,13 @@
-[default]
+[meta]
schedule = https://pretalx.c3voc.de/camp2023/schedule/export/schedule.xml
template = intro.mp4
alpha = false
prores = false
-fontfile = true
-inout = t
+inout_type = t
[title]
in = 16
out = 24
-fontfamily = BeonRegular
fontfile = saira.ttf
fontsize = 100
fontcolor = #FB48C4
@@ -19,7 +17,6 @@ y = 100
[speaker]
in = 16
out = 24
-fontfamily = BeonRegular
fontfile = saira.ttf
fontsize = 70
fontcolor = #3FFF21
@@ -29,11 +26,10 @@ y = 800
[text]
in = 16
out = 24
-fontfamily = BeonRegular
fontfile = saira.ttf
fontsize = 45
fontcolor = #FB48C4
x = (w-text_w)/2
y = 1000
-text = 'Chaos Communication Camp 2023'
+text = Chaos Communication Camp 2023
diff --git a/cccamp19/config.ini b/cccamp19/config.ini
index 181caed..d723d7d 100644
--- a/cccamp19/config.ini
+++ b/cccamp19/config.ini
@@ -1,15 +1,13 @@
-[default]
+[meta]
schedule = https://fahrplan.events.ccc.de/camp/2019/Fahrplan/schedule.xml
template = cccamp19_talks_intro_1080p.mov
alpha = true
prores = true
-fontfile = true
-inout = n
+inout_type = n
[title]
in = 193
out = 324
-fontfamily =
fontfile = Marvel-Bold.ttf
fontsize = 120
fontcolor = #c68100
@@ -19,7 +17,6 @@ y = 480
[speaker]
in = 233
out = 324
-fontfamily =
fontfile = Marvel-Regular.ttf
fontsize = 70
fontcolor = #c68100
@@ -29,11 +26,10 @@ y = 845
[text]
in = 242
out = 324
-fontfamily =
fontfile = Marvel-Regular.ttf
fontsize = 45
fontcolor = #c68100
x = (w-text_w)/2
y = 927
-text = 'chaos communication camp 2019'
+text = chaos communication camp 2019
diff --git a/denog11/config.ini b/denog11/config.ini
index 6f403b5..89c8096 100644
--- a/denog11/config.ini
+++ b/denog11/config.ini
@@ -1,15 +1,13 @@
-[default]
+[meta]
schedule = https://pretalx.denog.de/denog11/schedule/export/schedule.xml
template = denog11_intro_template.ts
alpha = false
prores = false
-fontfile = true
-inout = n
+inout_type = n
[title]
in = 1
out = 6.5
-fontfamily =
fontfile = DejaVuSans.ttf
fontsize = 100
fontcolor = #f9cc12
@@ -19,7 +17,6 @@ y = 200
[speaker]
in = 2
out = 6.5
-fontfamily =
fontfile = DejaVuSans.ttf
fontsize = 60
fontcolor = #ffffff
@@ -29,11 +26,10 @@ y = 900
[text]
in = 3
out = 6.5
-fontfamily =
fontfile = DejaVuSans.ttf
fontsize = 45
fontcolor = #ffffff
x = 640
y = 1000
-text = ''
+; text =
diff --git a/god2024/__init__.py b/god2024/__init__.py
new file mode 100644
index 0000000..c04c938
--- /dev/null
+++ b/god2024/__init__.py
@@ -0,0 +1,145 @@
+#!/usr/bin/python3
+
+from renderlib import *
+from schedulelib import *
+from easing import *
+
+# URL to Schedule-XML
+scheduleUrl = 'https://import.c3voc.de/schedule/god2024.xml?showall=yes'
+
+titlemap = {
+
+}
+
+def introFrames(p):
+ givenFrame = 0
+
+ nr = p['$id'];
+
+ # 1 Sekunden nix
+ frames = 1*fps
+ for i in range(0, frames):
+ givenFrame += 1
+ yield (
+ ('bg', 'attr', '{http://www.w3.org/1999/xlink}href', "given-frames/frame%04d.png" % (givenFrame)),
+ ('layer1', 'style', 'opacity', "%.4f" % 0), # nix
+ # ('text', 'attr', 'transform', 'translate(%.4f, 0)' % easeOutQuad(i, move, -move, frames)),
+ )
+
+ # 1 Sekunde Text Fadein
+ frames = 1*fps
+ for i in range(0, frames):
+ givenFrame += 1
+ yield (
+ ('bg', 'attr', '{http://www.w3.org/1999/xlink}href', "given-frames/frame%04d.png" % (givenFrame)),
+ ('layer1', 'style', 'opacity', "%.4f" % easeLinear(i, 0, 1, frames)),
+ # ('text', 'attr', 'transform', 'translate(%.4f, 0)' % easeOutQuad(i, move, -move, frames)),
+ )
+
+ # 5 Sekunden Text
+ frames = 5*fps
+ for i in range(0, frames):
+ givenFrame += 1
+ yield (
+ ('bg', 'attr', '{http://www.w3.org/1999/xlink}href', "given-frames/frame%04d.png" % (givenFrame)),
+ ('layer1', 'style', 'opacity', "%.4f" %1),
+ # ('text', 'attr', 'transform', 'translate(%.4f, 0)' % easeOutQuad(i, move, -move, frames)),
+ )
+
+def outroFrames(p):
+ xml = etree.parse('god2024/artwork/outro.svg').getroot()
+
+ frames = int(5*fps)
+ for i in range(0, frames):
+ yield ()
+
+def pauseFrames(p):
+ # 1 sekunden fade in
+ frames = 1*fps
+ for i in range(0, frames):
+ yield (
+ ('text1', 'style', 'opacity', "%.4f" % easeLinear(i, 0, 1, frames)),
+ )
+
+ # 1 sekunde sehen
+ for i in range(0, frames):
+ yield (
+ ('text1', 'style', 'opacity', "%.4f" % 1),
+ )
+
+ # 1 sekunde fadeout
+ for i in range(0, frames):
+ yield (
+ ('text1', 'style', 'opacity', "%.4f" % easeLinear(i, 1, -1, frames)),
+ )
+
+ # 1 sekunde bild
+ for i in range(0, frames):
+ yield (
+ ('text1', 'style', 'opacity', "%.4f" % 0),
+ )
+
+def debug():
+ render(
+ 'intro.svg',
+ '../intro.ts',
+ introFrames,
+ {
+ '$id': 65,
+ '$title': 'OWASP Juice Shop 10th anniversary: Is it still fresh?'.upper(),
+ '$subtitle': '',
+ '$personnames': 'Jannik Hollenbach'.upper(),
+ #'only_render_frame': 353
+ 'only_rerender_frames_after': 225
+ }
+ )
+
+ # render(
+ # 'pause.svg',
+ # '../pause.ts',
+ # pauseFrames
+ # )
+
+def tasks(queue, args, id_list, skip_list):
+ if not 'outro' in skip_list:
+ # place a task for the outro into the queue
+ queue.put(Rendertask(
+ infile = 'outro.svg',
+ outfile = 'outro.ts',
+ sequence = outroFrames
+ ))
+
+ if not 'pause' in skip_list:
+ # place the pause-sequence into the queue
+ queue.put(Rendertask(
+ infile = 'pause.svg',
+ outfile = 'pause.ts',
+ sequence = pauseFrames
+ ))
+
+ # iterate over all events extracted from the schedule xml-export
+ for event in events(scheduleUrl, titlemap):
+
+ # skip events which will not be recorded
+ if event['room'] not in ('Da Capo',) or event['track'] == 'Nomnom':
+ print("skipping room %s (%s [%s])" % (event['room'], event['title'], event['id']))
+ continue
+
+ # when id_list is not empty, only render events which are in id_list
+ if id_list and int(event['id']) not in id_list:
+ print("skipping id (%s [%s])" % (event['title'], event['id']))
+ continue
+
+ # generate a task description and put them into the queue
+ queue.put(Rendertask(
+ infile = 'intro.svg',
+ outfile = str(event['id']) + ".ts",
+ sequence = introFrames,
+ parameters = {
+ '$id': event['id'],
+ '$title': event['title'].upper(),
+ '$subtitle': event['subtitle'],
+ '$personnames': event['personnames'].upper(),
+ }
+ ))
+
diff --git a/god2024/artwork/intro.svg b/god2024/artwork/intro.svg
new file mode 100644
index 0000000..4a79ff0
--- /dev/null
+++ b/god2024/artwork/intro.svg
@@ -0,0 +1,101 @@
+
+
+
+
diff --git a/god2024/artwork/outro.svg b/god2024/artwork/outro.svg
new file mode 100644
index 0000000..8097ff8
--- /dev/null
+++ b/god2024/artwork/outro.svg
@@ -0,0 +1,131 @@
+
+
+
+
diff --git a/god2024/artwork/pause.svg b/god2024/artwork/pause.svg
new file mode 100644
index 0000000..6dd0f4e
--- /dev/null
+++ b/god2024/artwork/pause.svg
@@ -0,0 +1,78 @@
+
+
+
+
diff --git a/jh19-berlin/config.ini b/jh19-berlin/config.ini
index 6a8740d..73365a8 100644
--- a/jh19-berlin/config.ini
+++ b/jh19-berlin/config.ini
@@ -1,15 +1,13 @@
-[default]
+[meta]
schedule = https://projects.alpaka.space/media/jhber19-schedule.xml
template = intro-alpha.mov
alpha = true
prores = true
-fontfile = true
-inout = n
+inout_type = n
[title]
in = 175
out = 260
-fontfamily =
fontfile = SourceSansPro-Bold.otf
fontsize = 90
fontcolor = #ffffff
@@ -19,7 +17,6 @@ y = 450
[speaker]
in = 175
out = 260
-fontfamily =
fontfile = SourceSansPro-Regular.otf
fontsize = 36
fontcolor = #ffffff
@@ -29,11 +26,10 @@ y = 900
[text]
in = 200
out = 250
-fontfamily =
fontfile = SourceSansPro-Regular.otf
fontsize = 45
fontcolor = #c68100
x = (w-text_w)/2
y = 927
-text = ''
+; text =
diff --git a/jh20-jue/config.ini b/jh20-jue/config.ini
index 163ca71..05878cf 100644
--- a/jh20-jue/config.ini
+++ b/jh20-jue/config.ini
@@ -1,37 +1,35 @@
-[default]
+[meta]
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
; 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
alpha = false
prores = false
-; enable using a font file
-fontfile = true
; in and out time format: t for seconds, n for frame number
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]
; inframe for title
in = 20
-; outframe for totle
+; outframe for title
out = 225
-; title font (either font family or file, see default setting above)
-fontfamily =
-fontfile = ebisu.ttf
; title font size
fontsize = 70
-; title color
-fontcolor = #ffffff
-; title position from upper left corner
x = 600
y = 865
[speaker]
in = 40
out = 225
-fontfamily =
-fontfile = ebisu.ttf
fontsize = 40
fontcolor = #eeeeee
x = 600
@@ -41,13 +39,10 @@ y = 950
[text]
in = 3
out = 4
-fontfamily =
-fontfile = ebisu.ttf
fontsize = 45
-fontcolor = #ffffff
x = 640
y = 1000
-text = ''
+; text =
; build intros via
diff --git a/jh21-rn/config.ini b/jh21-rn/config.ini
index a7870db..626db55 100644
--- a/jh21-rn/config.ini
+++ b/jh21-rn/config.ini
@@ -1,15 +1,13 @@
-[default]
+[meta]
schedule = https://pretalx.c3voc.de/jugend-hackt-rhein-neckar-2021/schedule/export/schedule.xml
template = jh21-rn-template.ts
alpha = false
prores = false
-fontfile = true
-inout = t
+inout_type = t
[title]
in = 1
out = 6.5
-fontfamily =
fontfile = SourceSansPro-Semibold.ttf
fontsize = 85
fontcolor = #ffffff
@@ -19,7 +17,6 @@ y = 877
[speaker]
in = 2
out = 6.5
-fontfamily =
fontfile = SourceSansPro-Semibold.ttf
fontsize = 45
fontcolor = #ffffff
@@ -29,11 +26,10 @@ y = 954
[text]
in = 3
out = 6.5
-fontfamily =
fontfile = SourceSansPro-Semibold.ttf
fontsize = 45
fontcolor = #ffffff
x = 1920
y = 1080
-text = ''
+; text =
diff --git a/jugendhackt/config.ini b/jugendhackt/config.ini
index 2400940..94180a5 100644
--- a/jugendhackt/config.ini
+++ b/jugendhackt/config.ini
@@ -1,39 +1,52 @@
-[default]
+[meta]
schedule = https://pretalx.c3voc.de/jhhh23/schedule/export/schedule.xml
+;; path to background video
template = intro-background.ts
+;; whether background video uses transparency (needs to be .mov)
alpha = false
+;; whether background video is prores 4444
prores = false
-fontfile = true
-inout = t
+;; in and out time format: t for seconds, n for frame number
+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]
in = 1
out = 9.5
-fontfamily =
-fontfile = SourceSansPro-Semibold.ttf
fontsize = 67
-fontcolor = #ffffff
x = 400
y = 870
[speaker]
in = 2
out = 9
-fontfamily =
-fontfile = SourceSansPro-Semibold.ttf
fontsize = 50
-fontcolor = #ffffff
x = 400
y = 950
-
+;; optional extra text, comment out "text" field to disable
[text]
in = 0
out = 0
-fontfamily =
-fontfile = SourceSansPro-Semibold.ttf
fontsize = 0
-fontcolor = #ffffff
x = 0
y = 0
-text = ''
+;text = some additional text
diff --git a/jugendhackt/source.xcf b/jugendhackt/source.xcf
new file mode 100644
index 0000000..e01939d
Binary files /dev/null and b/jugendhackt/source.xcf differ
diff --git a/make-apple-motion.py b/make-apple-motion.py
index 1d4c631..c2940e9 100755
--- a/make-apple-motion.py
+++ b/make-apple-motion.py
@@ -11,8 +11,6 @@ import sys
import os
import re
-from xml.sax.saxutils import escape as xmlescape
-
# Parse arguments
parser = argparse.ArgumentParser(
description='C3VOC Intro-Outro-Generator - Variant to use with apple Motion Files',
@@ -57,6 +55,20 @@ parser.add_argument('--num-audio-streams', dest='naudio', type=int, default=1, h
number of audio-streams to generate. defaults to 1
''')
+parser.add_argument('--no-cleanup', action='store_true', help='''
+ keep temp-dir for debugging purposes
+ ''')
+
+parser.add_argument('--snapshot-sec', type=int, default=3, help='''
+ number of seconds into the final clip when to take a snapshot (for inspection purposes or as thumbnail)
+ ''')
+
+parser.add_argument('--setting-path', default='hd1080p.compressorsetting', help='''
+ filename in the script-dir (where this python script resides),
+ the work-dir (where the .motn-file resides) or absolute path to
+ a .compressorsetting file
+ ''')
+
args = parser.parse_args()
@@ -101,9 +113,22 @@ def describe_event(event):
def event_print(event, message):
print("{} – {}".format(describe_event(event), message))
+def find_settingpath():
+ artwork_dir = os.path.dirname(args.motn)
+ setting_path = os.path.join(artwork_dir, args.setting_path)
+ if os.path.exists(setting_path):
+ return setting_path
+
+ setting_path = os.path.join(os.path.dirname(__file__), args.setting_path)
+ if os.path.exists(setting_path):
+ return setting_path
+
+ return args.setting_path
+
tempdir = tempfile.TemporaryDirectory()
print('working in ' + tempdir.name)
+settingpath = find_settingpath()
def fmt_command(command, **kwargs):
@@ -128,6 +153,13 @@ def run_output(command, **kwargs):
os.system(f'{cmd} >{t.name} 2>&1')
return t.read().decode('utf-8')
+def xmlescape(xml):
+ xml = xml.replace("&", "&")
+ xml = xml.replace("<", "<")
+ xml = xml.replace(">", ">")
+ xml = xml.replace("\"", """)
+ xml = xml.replace("'", "'")
+ return xml
def enqueue_job(event):
event_id = str(event['id'])
@@ -144,10 +176,11 @@ def enqueue_job(event):
fp.write(xmlstr)
compressor_info = run_output(
- '/Applications/Compressor.app/Contents/MacOS/Compressor -batchname {batchname} -jobpath {jobpath} -settingpath hd1080p.compressorsetting -locationpath {locationpath}',
+ '/Applications/Compressor.app/Contents/MacOS/Compressor -batchname {batchname} -jobpath {jobpath} -settingpath {settingpath} -locationpath {locationpath}',
batchname=describe_event(event),
jobpath=work_doc,
- locationpath=intermediate_clip)
+ locationpath=intermediate_clip,
+ settingpath=settingpath)
match = re.search(r"", compressor_info)
if not match:
@@ -202,6 +235,7 @@ def finalize_job(job_id, event):
intermediate_clip = os.path.join(tempdir.name, event_id + '.mov')
final_clip = os.path.join(os.path.dirname(args.motn), event_id + '.ts')
copy_clip = os.path.join(os.path.dirname(args.motn), event_id + '.mov')
+ snapshot_file = os.path.join(os.path.dirname(args.motn), event_id + '.png')
shutil.copy(intermediate_clip, copy_clip)
@@ -211,6 +245,11 @@ def finalize_job(job_id, event):
vcodec=args.vcodec,
acodec=args.acodec)
+ run('ffmpeg -y -hide_banner -loglevel error -i {input} -ss {snapshot_sec} -frames:v 1 -vf scale="iw*sar:ih" -f image2 -y -c png {output}',
+ input=intermediate_clip,
+ output=snapshot_file,
+ snapshot_sec=str(args.snapshot_sec))
+
event_print(event, "finalized intro to " + final_clip)
@@ -250,5 +289,9 @@ while len(active_jobs) > 0:
finalize_job(job_id, event)
-print('all done, cleaning up ' + tempdir.name)
-tempdir.cleanup()
+if args.no_cleanup:
+ print('all done, *NOT* cleaning up, *TEMPFILES REMAIN* in ' + tempdir.name)
+
+else:
+ print('all done, cleaning up ' + tempdir.name)
+ tempdir.cleanup()
diff --git a/make-ffmpeg-fade.py b/make-ffmpeg-fade.py
index 976c858..3c1eecd 100755
--- a/make-ffmpeg-fade.py
+++ b/make-ffmpeg-fade.py
@@ -1,327 +1,5 @@
#!/usr/bin/env python3
# vim: tabstop=4 shiftwidth=4 expandtab
-import os
-import sys
-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')
+print("ERROR: The functionality of this script was added to 'make-ffmpeg.py'!")
+print("Specify meta.fade_duration = 0.5 in the config.ini for the same effect.")
diff --git a/make-ffmpeg.py b/make-ffmpeg.py
index fe4b745..94d1514 100755
--- a/make-ffmpeg.py
+++ b/make-ffmpeg.py
@@ -1,155 +1,167 @@
#!/usr/bin/env python3
# vim: tabstop=4 shiftwidth=4 expandtab
+"""See jugendhackt/config.ini for some config file documentation."""
+
import os
import sys
import subprocess
-import schedulelib
import argparse
-import shlex
-from PIL import ImageFont
-from configparser import ConfigParser
-import json
-import platform
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
-# 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 = []
+FRAME_WIDTH = 1920
-def headline(str):
+class TextConfig:
+ inpoint: float
+ outpoint: float
+ x: int
+ 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]
+
+
+class Config:
+ schedule: str
+ template_file: str # video background
+ 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
+
+
+def parse_config(filename) -> Config:
+ if not os.path.exists(filename):
+ error("config.ini file in Project Path is missing")
+
+ 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")
+
+ if not args.project:
+ error("The Project Path is a required argument")
+
+ if not args.debug and not conf.schedule:
+ error("Either specify --debug or supply a schedule in config.ini")
+
+ return conf
+
+
+def error(err_str):
print("##################################################")
- print(str)
+ print(err_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']
-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")
-
-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'])
@@ -158,164 +170,184 @@ 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)
+def fit_text(string: str, max_width: int, font: ImageFont) -> list[str]:
+ """Break text into array of strings which fit certain a width (in pixels) for the specified font."""
- 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 = ""
+ lines = []
w = 0
- line_num = 0
- line = ""
+ line = []
for word in split_line:
- left, top, right, bottom = translation_font.getbbox(" ".join([line, word]))
- width, height = right - left, bottom - top
- if width > (frame_width - (2 * 6)):
- lines += line.strip() + "\n"
- line = ""
+ new_line = line + [word.rstrip(':')]
+ w = font.getlength(" ".join(new_line))
+ if w > max_width:
+ lines.append(' '.join(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
-def fit_title(string: str, fontsize: int, x_offset: int):
- global translation_font
- translation_font = ImageFont.truetype(
- font_t, size=fontsize, encoding="unic")
- title = fit_text(string, (1920-x_offset-100))
+def ffmpeg_escape_str(text: str) -> str:
+ # Escape according to https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping
+ # and don't put the string in quotes afterwards!
+ text = text.replace(",", r"\,")
+ text = text.replace(':', r"\\:")
+ text = text.replace("'", r"\\\'")
- return title
+ return text
-def fit_speaker(string: str, fontsize: int, x_offset: int):
- 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):
+def enqueue_job(conf: Config, event):
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:
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:
+ if (os.path.exists(outfile) or os.path.exists(outfile_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, int(title_fontsize), int(title_x))
- t = t.replace(':', "\:") # the ffmpeg command needs colons to be escaped
- s = fit_speaker(event_personnames, int(speaker_fontsize), int(speaker_x))
+ title = conf.title.fit_text(event_title)
+ speakers = conf.speaker.fit_text(event_personnames)
+ extra_text = conf.text.fit_text(conf.extra_text)
if args.debug:
- print('Title: ', t)
- print('Speaker: ', s)
-
- outfile = os.path.join(os.path.dirname(args.project), event_id + '.ts')
+ print('Title: ', title)
+ print('Speaker: ', speakers)
if platform.system() == 'Windows':
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:
ffmpeg_path = 'ffmpeg'
- if fontfile == 'true':
- if platform.system() == 'Windows':
- 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_win, 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_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)
+ videofilter = conf.title.get_ffmpeg_filter(conf.inout_type, conf.fade_duration, title) + ","
+ videofilter += conf.speaker.get_ffmpeg_filter(conf.inout_type,
+ conf.fade_duration, speakers) + ","
+ videofilter += conf.text.get_ffmpeg_filter(conf.inout_type, conf.fade_duration, extra_text)
- if fileformat == '.mov':
- if alpha == 'true':
- if prores == 'true':
- cmd = '{3} -y -i "{0}" -vf "{1}" -vcodec prores_ks -pix_fmt yuva444p10le -profile:v 4444 -shortest -movflags faststart -f mov "{2}"'.format(
- infile, videofilter, outfile, ffmpeg_path)
- else:
- cmd = '{3} -y -i "{0}" -vf "{1}" -shortest -c:v qtrle -movflags faststart -f mov "{2}"'.format(
- infile, videofilter, outfile, ffmpeg_path)
+ cmd = [ffmpeg_path, '-y', '-i', conf.template_file, '-vf', videofilter]
+
+ if conf.fileext == '.mov' and conf.alpha:
+ if conf.prores:
+ cmd += ['-vcodec', 'prores_ks', '-pix_fmt', 'yuva444p10le', '-profile:v',
+ '4444', '-shortest', '-movflags', 'faststart', '-f', 'mov', outfile_mov]
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)
+ cmd += ['-shortest', '-c:v', 'qtrle', '-movflags',
+ 'faststart', '-f', 'mov', outfile_mov]
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)
+ cmd += ['-map', '0:0', '-c:v', 'mpeg2video', '-q:v', '2', '-aspect', '16:9', '-map',
+ '0:1', '-c:a', 'mp2', '-b:a', '384k', '-shortest', '-f', 'mpegts', outfile]
if args.debug:
print(cmd)
- run(cmd)
+ subprocess.check_call(cmd,
+ stderr=subprocess.STDOUT,
+ stdout=subprocess.DEVNULL
+ )
return event_id
-if args.ids:
- if len(args.ids) == 1:
- print("enqueuing {} job".format(len(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:
- print("enqueuing {} jobs".format(len(args.ids)))
-else:
- if len(events) == 1:
- print("enqueuing {} job".format(len(events)))
+ events = list(schedulelib.events(config.schedule))
+
+ if args.ids:
+ if len(args.ids) == 1:
+ print("enqueuing {} job".format(len(args.ids)))
+ else:
+ print("enqueuing {} jobs".format(len(args.ids)))
else:
- print("enqueuing {} jobs".format(len(events)))
+ 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
-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
- 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']))
- event_print(event, "enqueued as " + str(event['id']))
+ job_id = enqueue_job(config, event)
+ if not job_id:
+ event_print(event, "job was not enqueued successfully, skipping postprocessing")
+ continue
- job_id = enqueue_job(event)
- if not job_id:
- event_print(event, "job was not enqueued successfully, skipping postprocessing")
- continue
-
-
-print('all done')
+ print('all done')
diff --git a/mrmcd2019/config.ini b/mrmcd2019/config.ini
index 165753c..fb74efc 100755
--- a/mrmcd2019/config.ini
+++ b/mrmcd2019/config.ini
@@ -1,16 +1,14 @@
-[default]
+[meta]
#schedule = https://talks.mrmcd.net/2019/schedule/export/schedule.xml
schedule = file:///home/thorti/git/c3voc/intro-outro-generator/mrmcd2019/schedule.xml
template = mrmcd2019.mov
alpha = false
prores = false
-fontfile = true
-inout = n
+inout_type = n
[title]
in = 50
out = 225
-fontfamily =
fontfile = Jura-Bold.ttf
fontsize = 80
fontcolor = #47acda
@@ -20,7 +18,6 @@ y = 540
[speaker]
in = 75
out = 225
-fontfamily =
fontfile = Jura-Regular.ttf
fontsize = 50
fontcolor = #094762
@@ -30,10 +27,9 @@ y = 950
[text]
in = 242
out = 324
-fontfamily =
fontfile = Jura-Regular.ttf
fontsize = 45
fontcolor = #c68100
x = (w-text_w)/2
y = 927
-text = ''
+; text =
diff --git a/osmodevcon24/__init__.py b/osmodevcon24/__init__.py
new file mode 100644
index 0000000..4264294
--- /dev/null
+++ b/osmodevcon24/__init__.py
@@ -0,0 +1,148 @@
+#!/usr/bin/python3
+
+from renderlib import *
+from schedulelib import *
+from easing import *
+
+# URL to Schedule-XML
+scheduleUrl = 'https://pretalx.sysmocom.de/osmodevcon2024/schedule/export/schedule.xml'
+
+def introFrames(args):
+ #fade in title
+ frames = 3*fps
+ for i in range(0, frames):
+ yield(
+ ('title', 'style', 'opacity', easeInQuad(i, 0, 1, frames)),
+ )
+ # fade in subtitle and names
+ frames = 1*fps
+ for i in range(0, frames):
+ yield(
+ ('title', 'style', 'opacity', 1),
+ ('subtitle', 'style', 'opacity', easeInQuad(i, 0, 1, frames)),
+ ('personnames', 'style', 'opacity', easeInQuad(i, 0, 1, frames)),
+ )
+ #show whole image for 2 seconds
+ frames = 2*fps
+ for i in range(0, frames):
+ yield(
+ ('title', 'style', 'opacity', 1),
+ ('personnames', 'style', 'opacity', 1),
+ ('subtitle', 'style', 'opacity', 1),
+ )
+
+def backgroundFrames(parameters):
+ frames = 5*fps
+ for i in range(0, frames):
+ yield(
+ ('logo', 'style', 'opacity', 1),
+ )
+
+def outroFrames(args):
+ frames = 2*fps
+ for i in range(0, frames):
+ yield(
+ ('logo', 'style', 'opacity', 1),
+ ('sublogo', 'style', 'opacity', 1),
+ ('cclogo', 'style', 'opacity', 1),
+ )
+ # fade out
+ frames = 3*fps
+ for i in range(0, frames):
+ yield(
+ ('logo', 'style', 'opacity', "%.4f" % easeInCubic(i, 1, -1, frames)),
+ ('sublogo', 'style', 'opacity', "%.4f" % easeInCubic(i, 1, -1, frames)),
+ ('cclogo', 'style', 'opacity', "%.4f" % easeInCubic(i, 1, -1, frames)),
+ )
+
+def pauseFrames(args):
+ #fade in pause
+ frames = 4*fps
+ for i in range(0, frames):
+ yield(
+ ('pause', 'style', 'opacity', "%.4f" % easeInCubic(i, 0.2, 1, frames)),
+ )
+
+ # fade out
+ frames = 4*fps
+ for i in range(0, frames):
+ yield(
+ ('pause', 'style', 'opacity', "%.4f" % easeInCubic(i, 1, -0.8, frames)),
+ )
+
+def debug():
+ render('intro.svg',
+ '../intro.ts',
+ introFrames,
+ {
+ '$id': 7776,
+ '$title': 'Configuring + running GPRS/EDGE data services with OsmoPCU, OsmoSGSN and OpenGGSN',
+ '$subtitle': 'With some subtitle!',
+ '$personnames': 'Alexander Chemeris + Harald Welte'
+ }
+ )
+
+ render('outro.svg',
+ '../outro.ts',
+ outroFrames
+ )
+
+ render(
+ 'background.svg',
+ '../background.ts',
+ backgroundFrames
+ )
+
+ render('pause.svg',
+ '../pause.ts',
+ pauseFrames
+ )
+
+
+def tasks(queue, args, idlist, skiplist):
+ # iterate over all events extracted from the schedule xml-export
+ for event in events(scheduleUrl):
+ if not (idlist==[]):
+ if 000000 in idlist:
+ print("skipping id (%s [%s])" % (event['title'], event['id']))
+ continue
+ if int(event['id']) not in idlist:
+ print("skipping id (%s [%s])" % (event['title'], event['id']))
+ continue
+
+ # generate a task description and put them into the queue
+ queue.put(Rendertask(
+ infile = 'intro.svg',
+ outfile = str(event['id'])+".ts",
+ sequence = introFrames,
+ parameters = {
+ '$id': event['id'],
+ '$title': event['title'],
+ '$subtitle': event['subtitle'],
+ '$personnames': event['personnames']
+ }
+ ))
+
+ # place a task for the outro into the queue
+ if not "out" in skiplist:
+ queue.put(Rendertask(
+ infile = 'outro.svg',
+ outfile = 'outro.ts',
+ sequence = outroFrames
+ ))
+
+ # place the pause-sequence into the queue
+ if not "pause" in skiplist:
+ queue.put(Rendertask(
+ infile = 'pause.svg',
+ outfile = 'pause.ts',
+ sequence = pauseFrames
+ ))
+
+ # place the background-sequence into the queue
+ if not "bg" in skiplist:
+ queue.put(Rendertask(
+ infile = 'background.svg',
+ outfile = 'background.ts',
+ sequence = backgroundFrames
+ ))
diff --git a/osmodevcon24/artwork/background.svg b/osmodevcon24/artwork/background.svg
new file mode 100644
index 0000000..3a87e75
--- /dev/null
+++ b/osmodevcon24/artwork/background.svg
@@ -0,0 +1,946 @@
+
+
+
+
diff --git a/osmodevcon24/artwork/intro.svg b/osmodevcon24/artwork/intro.svg
new file mode 100644
index 0000000..2a8b797
--- /dev/null
+++ b/osmodevcon24/artwork/intro.svg
@@ -0,0 +1,1372 @@
+
+
+
+
diff --git a/osmodevcon24/artwork/outro.svg b/osmodevcon24/artwork/outro.svg
new file mode 100644
index 0000000..1fbdc0e
--- /dev/null
+++ b/osmodevcon24/artwork/outro.svg
@@ -0,0 +1,1479 @@
+
+
+
+
diff --git a/osmodevcon24/artwork/pause.svg b/osmodevcon24/artwork/pause.svg
new file mode 100644
index 0000000..e8d1e7b
--- /dev/null
+++ b/osmodevcon24/artwork/pause.svg
@@ -0,0 +1,1208 @@
+
+
+
+
diff --git a/requirements.txt b/requirements.txt
index 181482a..efa6214 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
-pillow
+pillow>=8.0.0
cssutils==1.0.2
-lxml==4.9.1
-svg.path==4.0.2
-Wand==0.6.5
+lxml~=5.3
+svg.path~=6.0
+Wand~=0.6.5
diff --git a/schedulelib.py b/schedulelib.py
index 340e4d8..a986498 100644
--- a/schedulelib.py
+++ b/schedulelib.py
@@ -100,6 +100,12 @@ def events(scheduleUrl, titlemap={}):
url = event.find('url').text.strip()
else:
url = ''
+
+ if event.find('track') is not None and event.find('track').text is not None:
+ track = event.find('track').text
+ else:
+ track = ''
+
# yield a tupel with the event-id, event-title and person-names
yield {
'day': day.get('index'),
@@ -109,7 +115,7 @@ def events(scheduleUrl, titlemap={}):
'persons': personnames,
'personnames': ', '.join(personnames),
'room': room.attrib['name'],
- 'track': event.find('track').text,
+ 'track': track,
'url': url
}
diff --git a/vcfb24/Computerfont.ttf b/vcfb24/Computerfont.ttf
new file mode 100644
index 0000000..49f1c62
Binary files /dev/null and b/vcfb24/Computerfont.ttf differ
diff --git a/vcfb24/SourceSansPro-Semibold.ttf b/vcfb24/SourceSansPro-Semibold.ttf
new file mode 100644
index 0000000..bf69cc2
Binary files /dev/null and b/vcfb24/SourceSansPro-Semibold.ttf differ
diff --git a/vcfb24/config.ini b/vcfb24/config.ini
new file mode 100644
index 0000000..712eff6
--- /dev/null
+++ b/vcfb24/config.ini
@@ -0,0 +1,37 @@
+[meta]
+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
+template = intro.mp4
+alpha = false
+prores = false
+inout_type = t
+fade_duration = 0.5
+
+[title]
+in = 1
+out = 9.5
+fontfile = Computerfont.ttf
+fontsize = 100
+fontcolor = #ffffff
+x = 85
+y = 122
+
+[speaker]
+in = 2
+out = 9
+fontfile = SourceSansPro-Semibold.ttf
+fontsize = 65
+fontcolor = #ffffff
+x = 85
+y = 861
+
+
+[text]
+in = 0
+out = 0
+fontfile = Computerfont.ttf
+fontsize = 0
+fontcolor = #ffffff
+x = 0
+y = 0
+; text =
diff --git a/vcfb24/intro.svg b/vcfb24/intro.svg
new file mode 100644
index 0000000..30044ab
--- /dev/null
+++ b/vcfb24/intro.svg
@@ -0,0 +1,15584 @@
+
+
+
+
diff --git a/vcfb24/lower-third.svg b/vcfb24/lower-third.svg
new file mode 100644
index 0000000..a70d3b4
--- /dev/null
+++ b/vcfb24/lower-third.svg
@@ -0,0 +1,6231 @@
+
+
+
+
diff --git a/vcfb24/outro.svg b/vcfb24/outro.svg
new file mode 100644
index 0000000..45f706d
--- /dev/null
+++ b/vcfb24/outro.svg
@@ -0,0 +1,1668 @@
+
+
+
\ No newline at end of file
diff --git a/vcfb24/pause.svg b/vcfb24/pause.svg
new file mode 100644
index 0000000..8ef961c
--- /dev/null
+++ b/vcfb24/pause.svg
@@ -0,0 +1,6626 @@
+
+
+
+
\ No newline at end of file