Compare commits

..

25 commits

Author SHA1 Message Date
Franziska Kunsmann
bfbdda8ffe
Merge pull request #70 from MaZderMind/t4at-2024-motn-fixes
t4at 2024 motn fixes
2024-11-21 20:11:48 +01:00
Peter Körner
21a8e014ef make-apple-motion: replace xml.sax.saxutils.xmlescape with custom routine, which also escapes quotes
xml.sax.saxutils.xmlescape is only a str.replace in a trencoat anyways.
2024-11-21 19:37:35 +01:00
Peter Körner
a9cbfc5eb3 make-apple-motion: add --snapshot-sec to automatically generate a snapshot of the final clip for inspection or as thumbnail 2024-11-21 19:37:33 +01:00
Peter Körner
41db29d24b make-apple-motion: add --setting-path flag and auto-detect absolute path to default .compressorsetting file 2024-11-21 19:37:08 +01:00
Peter Körner
40eb76b968 make-apple-motion: add --no-cleanup flag 2024-11-21 19:37:03 +01:00
Lukas Schauer
5661e690de
god2024 changes 2024-11-12 12:43:11 +01:00
Lukas Schauer
d8c198f984
god2024 2024-11-12 11:38:27 +01:00
Jannik Beyerstedt
b3fb07a6e3 Merge branch 'feat/refactor-make-ffmpeg' 2024-11-03 21:35:20 +01:00
Jannik Beyerstedt
2451c0f25f bump some versions in requirements.txt
- lxml 5 works
- svg.path has no breaking changes according to change log
- cssutils has a v2, but didn't find a changelog
2024-11-03 21:32:53 +01:00
Jannik Beyerstedt
ca224b9d84 make-ffmpeg: Remove fontfamily option as it doesn't work 2024-11-03 21:32:53 +01:00
Jannik Beyerstedt
8933550f31 make-ffmpeg: Combine with make-ffmpeg-fade 2024-11-03 21:32:53 +01:00
Jannik Beyerstedt
31f01ca386 make-ffmpeg: Use fit_text from make-ffmpeg-fade (break into lines array) 2024-11-03 21:32:51 +01:00
Jannik Beyerstedt
6876f2cf44 make-ffmpeg: Rework config file 2024-11-03 21:25:16 +01:00
Jannik Beyerstedt
526ef6231a make-ffmpeg: add border option 2024-11-02 20:57:55 +01:00
Jannik Beyerstedt
9210d36248 make-ffmpeg: Properly escape special characters 2024-11-02 20:57:55 +01:00
Jannik Beyerstedt
0bb26d6b25 make-ffmpeg: Refactor code 2024-11-02 20:57:48 +01:00
Franziska Kunsmann
26c660a135
vcfb24 switch to make-ffmpeg-fade 2024-10-19 11:00:23 +02:00
Franziska Kunsmann
8ca5d334cb
vcfb24 fix imports 2024-10-19 10:45:29 +02:00
Franziska Kunsmann
367e8ea094
re-add jugendhackt source gimp file 2024-10-19 10:33:17 +02:00
Franziska Kunsmann
c94b840807
vcfb24 2024-10-19 10:31:32 +02:00
Jannik Beyerstedt
471e0c38b7 Merge remote-tracking branch 'github/master' 2024-09-21 22:36:33 +02:00
Jannik Beyerstedt
848b72b113 schedulelib: Ignore missing track field
According to the XML schema, the "track" field of an event is not mandatory. So we should replace it with an empty string if not existing like we do for other fields.
2024-09-01 16:36:15 +02:00
Sophie Schiller
1893beda4d
Merge pull request #64 from fkusei/master
fix make-ffmpeg-fade
2024-04-04 08:30:06 +02:00
Franziska Kunsmann
ec29473d53 make-ffmpeg-fade: use better algorythm for determining lines 2024-03-18 14:19:36 +01:00
Franziska Kunsmann
96f410ba6a make-ffmpeg-fade: fix missing import 2024-03-18 14:19:08 +01:00
30 changed files with 30992 additions and 708 deletions

View file

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

View file

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

View file

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

145
god2024/__init__.py Normal file
View file

@ -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(),
}
))

101
god2024/artwork/intro.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.9 MiB

131
god2024/artwork/outro.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.4 MiB

78
god2024/artwork/pause.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

View file

@ -1,39 +0,0 @@
[default]
schedule = https://sessions.hack-mas.at/api/0/xml/schedule
template = 290824_Hackmas_intro_video_v3.mp4
alpha = false
prores = false
fontfile = true
inout = t
[title]
in = 13
out = 21
fontfamily =
fontfile = VDS_New.ttf
fontsize = 75
fontcolor = #ffffff
x = 1100
y = 550
[speaker]
in = 2
out = 9
fontfamily =
fontfile = VDS_New.ttf
fontsize = 64
fontcolor = #ffffff
x = 250
y = 1080
[text]
in = 0
out = 0
fontfamily =
fontfile = VDS_New.ttf
fontsize = 0
fontcolor = #ffffff
x = 0
y = 0
text = ''

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

View file

@ -1,8 +0,0 @@
# Hackmas 2024 Readme
Get intro-slate from shared storage (provided by event).
Get outro.ts from shared storage or generate from still images by running:
```sh
ffmpeg -loop 1 -i hackmas2024-outro-slate.png -f lavfi -i anullsrc -c:v mpeg2video -t 10 -aspect 16:9 -c:a mp2 -b:v 15000k -map 0:v -map 1:a outro.ts
```

View file

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

View file

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

View file

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

View file

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

BIN
jugendhackt/source.xcf Normal file

Binary file not shown.

View file

@ -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("<", "&lt;")
xml = xml.replace(">", "&gt;")
xml = xml.replace("\"", "&quot;")
xml = xml.replace("'", "&apos;")
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"<jobID ([A-Z0-9\-]+) ?\/>", 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()

View file

@ -1,329 +1,5 @@
#!/usr/bin/env python3
# vim: tabstop=4 shiftwidth=4 expandtab
import os
import sys
import subprocess
import renderlib
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:
w, _ = translation_font.getsize(" ".join([line, word]))
print("{}, {}".format(w, line))
if w > (frame_width):
print("too wide, breaking")
lines += line.strip() + "\n"
line = ""
line += word + " "
lines += line.strip()
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 = "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,
text=t)
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
)
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,
text=s)
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
)
videofilter += "drawtext=fontfile={fontfile}:fontsize={fontsize}:fontcolor={fontcolor}:x={x}:y={y}:text={text}:".format(
fontfile=font_tt,
fontsize=text_fontsize,
fontcolor=text_fontcolor,
x=text_x,
y=text_y,
text=text_text)
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=text_in,
fade_in_end_time=text_in + fade_duration,
fade_out_start_time=text_in + fade_duration + text_duration,
fade_out_end_time=text_in + fade_duration + text_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.")

View file

@ -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,169 +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
t = t.replace('\'', "") # the ffmpeg command needs ' to be escaped
s = fit_speaker(event_personnames, int(speaker_fontsize), int(speaker_x))
s = s.replace('\'', "") # the ffmpeg command needs ' to be escaped
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'
title_bordersize = int(title_fontsize) / 30
speaker_bordersize = int(speaker_fontsize) / 30
text_bordersize = int(text_fontsize) / 30
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}:borderw={9}:x={5}:y={6}:text='{7}',".format(
title_in, title_out, font_t, title_fontsize, title_fontcolor, title_x, title_y, t, inout, title_bordersize)
videofilter += "drawtext=enable='between({8},{0},{1})':fontfile='{2}':fontsize={3}:fontcolor={4}:borderw={9}:x={5}:y={6}:text='{7}',".format(
speaker_in, speaker_out, font_s, speaker_fontsize, speaker_fontcolor, speaker_x, speaker_y, s, inout, speaker_bordersize)
videofilter += "drawtext=enable='between({8},{0},{1})':fontfile='{2}':fontsize={3}:fontcolor={4}:borderw={9}:x={5}:y={6}:text='{7}'".format(
text_in, text_out, font_tt, text_fontsize, text_fontcolor, text_x, text_y, text_text, inout, text_bordersize)
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')

View file

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

View file

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

View file

@ -101,7 +101,7 @@ def events(scheduleUrl, titlemap={}):
else:
url = ''
if event.find('track') is not None:
if event.find('track') is not None and event.find('track').text is not None:
track = event.find('track').text
else:
track = ''

BIN
vcfb24/Computerfont.ttf Normal file

Binary file not shown.

Binary file not shown.

37
vcfb24/config.ini Normal file
View file

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

15584
vcfb24/intro.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.1 MiB

6231
vcfb24/lower-third.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1 MiB

1668
vcfb24/outro.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 935 KiB

6626
vcfb24/pause.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1,015 KiB