Compare commits

..

6 commits

Author SHA1 Message Date
Jannik Beyerstedt
474d793d31 hackmas2024: New intro and outro 2024-08-30 13:22:27 +02:00
Jannik Beyerstedt
7ef49366f1 hackmas2024: No speaker names 2024-08-29 22:04:41 +02:00
Jannik Beyerstedt
ab3da26e45 Hotfix: Replace ' by a real apostrophe 2024-08-29 22:04:23 +02:00
Jannik Beyerstedt
f6977c2c5d Hotfix: Ignore missing track field 2024-08-29 13:52:44 +02:00
Jannik Beyerstedt
108ef79e8d hackmas: patch in font border 2024-08-28 16:22:17 +02:00
Jannik Beyerstedt
957a4dd4a4 hackmas2024: init 2024-08-28 12:26:45 +02:00
30 changed files with 713 additions and 30997 deletions

View file

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

View file

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

View file

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

View file

@ -1,145 +0,0 @@
#!/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(),
}
))

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.9 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.4 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.9 MiB

BIN
hackmas2024/VDS_New.ttf Normal file

Binary file not shown.

39
hackmas2024/config.ini Normal file
View file

@ -0,0 +1,39 @@
[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.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

8
hackmas2024/readme.md Normal file
View file

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

View file

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

View file

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

View file

@ -1,52 +1,39 @@
[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
;; 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]
;; default font schedule = https://pretalx.c3voc.de/jhhh23/schedule/export/schedule.xml
fontfile = SourceSansPro-Semibold.ttf template = intro-background.ts
;; default font color alpha = false
fontcolor = #ffffff prores = false
fontfile = true
inout = t
;; fields for title and speaker names are empty in the template.ts, so we'll render them in via ffmpeg
;; parameters are:
;; - in: start frame/ time
;; - out: end frame/ time
;; - fontfile: font file
;; - fontcolor: font color
;; - fontsize: font size (pixel)
;; - x: horizontal position (top left corner)
;; - y: vertical position (top left corner)
[title] [title]
in = 1 in = 1
out = 9.5 out = 9.5
fontfamily =
fontfile = SourceSansPro-Semibold.ttf
fontsize = 67 fontsize = 67
fontcolor = #ffffff
x = 400 x = 400
y = 870 y = 870
[speaker] [speaker]
in = 2 in = 2
out = 9 out = 9
fontfamily =
fontfile = SourceSansPro-Semibold.ttf
fontsize = 50 fontsize = 50
fontcolor = #ffffff
x = 400 x = 400
y = 950 y = 950
;; optional extra text, comment out "text" field to disable
[text] [text]
in = 0 in = 0
out = 0 out = 0
fontfamily =
fontfile = SourceSansPro-Semibold.ttf
fontsize = 0 fontsize = 0
fontcolor = #ffffff
x = 0 x = 0
y = 0 y = 0
;text = some additional text text = ''

Binary file not shown.

View file

@ -11,6 +11,8 @@ import sys
import os import os
import re import re
from xml.sax.saxutils import escape as xmlescape
# Parse arguments # Parse arguments
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='C3VOC Intro-Outro-Generator - Variant to use with apple Motion Files', description='C3VOC Intro-Outro-Generator - Variant to use with apple Motion Files',
@ -55,20 +57,6 @@ parser.add_argument('--num-audio-streams', dest='naudio', type=int, default=1, h
number of audio-streams to generate. defaults to 1 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() args = parser.parse_args()
@ -113,22 +101,9 @@ def describe_event(event):
def event_print(event, message): def event_print(event, message):
print("{} {}".format(describe_event(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() tempdir = tempfile.TemporaryDirectory()
print('working in ' + tempdir.name) print('working in ' + tempdir.name)
settingpath = find_settingpath()
def fmt_command(command, **kwargs): def fmt_command(command, **kwargs):
@ -153,13 +128,6 @@ def run_output(command, **kwargs):
os.system(f'{cmd} >{t.name} 2>&1') os.system(f'{cmd} >{t.name} 2>&1')
return t.read().decode('utf-8') 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): def enqueue_job(event):
event_id = str(event['id']) event_id = str(event['id'])
@ -176,11 +144,10 @@ def enqueue_job(event):
fp.write(xmlstr) fp.write(xmlstr)
compressor_info = run_output( compressor_info = run_output(
'/Applications/Compressor.app/Contents/MacOS/Compressor -batchname {batchname} -jobpath {jobpath} -settingpath {settingpath} -locationpath {locationpath}', '/Applications/Compressor.app/Contents/MacOS/Compressor -batchname {batchname} -jobpath {jobpath} -settingpath hd1080p.compressorsetting -locationpath {locationpath}',
batchname=describe_event(event), batchname=describe_event(event),
jobpath=work_doc, jobpath=work_doc,
locationpath=intermediate_clip, locationpath=intermediate_clip)
settingpath=settingpath)
match = re.search(r"<jobID ([A-Z0-9\-]+) ?\/>", compressor_info) match = re.search(r"<jobID ([A-Z0-9\-]+) ?\/>", compressor_info)
if not match: if not match:
@ -235,7 +202,6 @@ def finalize_job(job_id, event):
intermediate_clip = os.path.join(tempdir.name, event_id + '.mov') intermediate_clip = os.path.join(tempdir.name, event_id + '.mov')
final_clip = os.path.join(os.path.dirname(args.motn), event_id + '.ts') 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') 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) shutil.copy(intermediate_clip, copy_clip)
@ -245,11 +211,6 @@ def finalize_job(job_id, event):
vcodec=args.vcodec, vcodec=args.vcodec,
acodec=args.acodec) 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) event_print(event, "finalized intro to " + final_clip)
@ -289,9 +250,5 @@ while len(active_jobs) > 0:
finalize_job(job_id, event) finalize_job(job_id, event)
if args.no_cleanup:
print('all done, *NOT* cleaning up, *TEMPFILES REMAIN* in ' + tempdir.name)
else:
print('all done, cleaning up ' + tempdir.name) print('all done, cleaning up ' + tempdir.name)
tempdir.cleanup() tempdir.cleanup()

View file

@ -1,5 +1,329 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# vim: tabstop=4 shiftwidth=4 expandtab # vim: tabstop=4 shiftwidth=4 expandtab
print("ERROR: The functionality of this script was added to 'make-ffmpeg.py'!") import os
print("Specify meta.fade_duration = 0.5 in the config.ini for the same effect.") 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')

View file

@ -1,270 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# vim: tabstop=4 shiftwidth=4 expandtab # vim: tabstop=4 shiftwidth=4 expandtab
"""See jugendhackt/config.ini for some config file documentation."""
import os import os
import sys import sys
import subprocess import subprocess
import argparse
import ssl
from configparser import ConfigParser
from pathlib import PurePath
import platform
from PIL import ImageFont
import schedulelib import schedulelib
import argparse
import shlex
from PIL import ImageFont
from configparser import ConfigParser
import json
import platform
import ssl
ssl._create_default_https_context = ssl._create_unverified_context ssl._create_default_https_context = ssl._create_unverified_context
FRAME_WIDTH = 1920
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(err_str)
print("##################################################")
print()
parser.print_help()
sys.exit(1)
def describe_event(event):
return "#{}: {}".format(event['id'], event['title'])
def event_print(event, message):
print("{} {}".format(describe_event(event), message))
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."""
split_line = [x.strip() for x in string.split()]
lines = []
w = 0
line = []
for word in split_line:
new_line = line + [word.rstrip(':')]
w = font.getlength(" ".join(new_line))
if w > max_width:
lines.append(' '.join(line))
line = []
line.append(word.rstrip(':'))
if word.endswith(':'):
lines.append(' '.join(line))
line = []
if line:
lines.append(' '.join(line))
return lines
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 text
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(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'])
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: ', title)
print('Speaker: ', speakers)
if platform.system() == 'Windows':
ffmpeg_path = './ffmpeg.exe'
else:
ffmpeg_path = 'ffmpeg'
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)
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 += ['-shortest', '-c:v', 'qtrle', '-movflags',
'faststart', '-f', 'mov', outfile_mov]
else:
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)
subprocess.check_call(cmd,
stderr=subprocess.STDOUT,
stdout=subprocess.DEVNULL
)
return event_id
if __name__ == "__main__":
# Parse arguments # Parse arguments
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='C3VOC Intro-Outro-Generator - Variant which renders only using video filters in ffmpeg', description='C3VOC Intro-Outro-Generator - Variant which renders only using video filters in ffmpeg',
@ -308,13 +57,89 @@ if __name__ == "__main__":
if (args.skip is None): if (args.skip is None):
args.skip = [] args.skip = []
config = parse_config(PurePath(args.project, 'config.ini'))
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']
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: if args.debug:
persons = ['Thomas Roth', 'Dmitry Nedospasov', 'Josh Datko',] persons = ['Thomas Roth', 'Dmitry Nedospasov', 'Josh Datko',]
events = [{ events = [{
'id': 'debug', 'id': 'debug',
'title': 'wallet.fail and the longest talk title to test if the template is big enough', 'title': 'wallet.fail',
'subtitle': 'Hacking the most popular cryptocurrency hardware wallets', 'subtitle': 'Hacking the most popular cryptocurrency hardware wallets',
'persons': persons, 'persons': persons,
'personnames': ', '.join(persons), 'personnames': ', '.join(persons),
@ -322,7 +147,153 @@ if __name__ == "__main__":
}] }]
else: else:
events = list(schedulelib.events(config.schedule)) 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:
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 = ""
line += word + " "
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))
return title
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):
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, 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
if args.debug:
print('Title: ', t)
print('Speaker: ', s)
outfile = os.path.join(os.path.dirname(args.project), event_id + '.ts')
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)
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)
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)
else:
cmd = '{3} -y -i "{0}" -vf "{1}" -map 0:0 -c:v mpeg2video -q:v 2 -aspect 16:9 -map 0:1 -c:a mp2 -b:a 384k -shortest -f mpegts "{2}"'.format(
infile, videofilter, outfile, ffmpeg_path)
if args.debug:
print(cmd)
run(cmd)
return event_id
if args.ids: if args.ids:
if len(args.ids) == 1: if len(args.ids) == 1:
@ -335,6 +306,7 @@ if __name__ == "__main__":
else: else:
print("enqueuing {} jobs".format(len(events))) print("enqueuing {} jobs".format(len(events)))
for event in events: for event in events:
if args.ids and event['id'] not in args.ids: if args.ids and event['id'] not in args.ids:
continue continue
@ -345,9 +317,10 @@ if __name__ == "__main__":
event_print(event, "enqueued as " + str(event['id'])) event_print(event, "enqueued as " + str(event['id']))
job_id = enqueue_job(config, event) job_id = enqueue_job(event)
if not job_id: if not job_id:
event_print(event, "job was not enqueued successfully, skipping postprocessing") event_print(event, "job was not enqueued successfully, skipping postprocessing")
continue continue
print('all done') print('all done')

View file

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

View file

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

View file

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

Binary file not shown.

Binary file not shown.

View file

@ -1,37 +0,0 @@
[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 =

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 935 KiB

File diff suppressed because one or more lines are too long

Before

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