intro-outro-generator/make-blender.py
2023-10-22 19:15:04 +02:00

347 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# vim: tabstop=4 shiftwidth=4 expandtab
"""
Generate intros by rendering a blender project with some text fields replaced by
the schedule contents.
The blender project must be configured to use mkv/matroska as output file format!
"""
import subprocess
import renderlib
import argparse
import tempfile
import shlex
import time
import sys
import os
import platform
from shutil import copyfile
titlemap = {}
#titlemap = {
# "205" : {
# "title" : "Attacking CPUs with Power Side Channels from Software"
# },
# "20" : {
# "title" : "New title for Talk id 20"
# }
#}
# Parse arguments
parser = argparse.ArgumentParser(
description='C3VOC Intro-Outro-Generator - Variant to use with Blender Files',
usage="./make-blender.py yourproject/ https://url/to/schedule.xml",
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('project', action="store", metavar='Project folder', type=str, help='''
Path to your project folder with Blender Files (intro.blend)
''')
parser.add_argument('schedule', action="store", metavar='Schedule-URL', type=str, nargs='?', help='''
URL or Path to your schedule.xml
''')
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. Schedule-URL can be left blank when
used with --debug
This argument must not be used together with --id
Usage: ./make-blender.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-blender.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-blender.py yourproject/ --room "HfG_Studio" "ZKM_Vortragssaal"
''')
parser.add_argument('--day', dest='days', nargs='+', action="store", type=str, help='''
Only render from your projects schedule for the given days.
This argument must not be used together with --debug
Usage: ./make-blender.py yourproject/ --day "1" "3"
''')
parser.add_argument('--pause', action="store_true", default=False, help='''
Render a pause loop from the pause.blend file in the project folder.
''')
parser.add_argument('--alpha', action="store_true", default=False, help='''
Render intro/outro with alpha.
''')
parser.add_argument('--force', action="store_true", default=False, help='''
Force render if file exists.
''')
parser.add_argument('--no-finalize', dest='nof', action="store_true", default=False, help='''
Skip finalize job.
''')
parser.add_argument('--outro', action="store_true", default=False, help='''
Render outro from the outro.blend file in the project folder.
''')
parser.add_argument('--bgloop', action="store_true", default=False, help='''
Render background loop from the bgloop.blend file in the project folder.
''')
parser.add_argument('--keep', action="store_true", default=False, help='''
Keep source file in the project folder after render.
''')
args = parser.parse_args()
def headline(str):
print("##################################################")
print(str)
print("##################################################")
print()
def error(str):
headline(str)
parser.print_help()
sys.exit(1)
if not args.project:
error("The Path to your project with Blender Files is a required argument")
if not args.debug and not args.pause and not args.outro and not args.bgloop and not args.schedule:
error("Either specify --debug, --pause, --outro or supply a schedule")
if args.debug:
#persons = ['blubbel']
persons = ['Vitor Sakaguti', 'Sara', 'A.L. Fehlhaber']
events = [{
'id': 11450,
'title': 'PQ Mail: Enabling post quantum secure encryption for email communication',
'subtitle': '',
'persons': persons,
'personnames': ', '.join(persons),
'room': 'rc1',
}]
elif args.pause:
events = [{
'id': 'pause',
'title': 'Pause Loop',
}]
elif args.outro:
events = [{
'id': 'outro',
'title': 'Outro',
}]
elif args.bgloop:
events = [{
'id': 'bgloop',
'title': 'Background Loop',
}]
else:
events = list(renderlib.events(args.schedule))
def describe_event(event):
return "#{}: {}".format(event['id'], event['title'])
def event_print(event, message):
print("{} {}".format(describe_event(event), message))
tempdir = tempfile.TemporaryDirectory()
print('working in ' + tempdir.name)
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 run_show(command, **kwargs):
return subprocess.check_call(
fmt_command(command, **kwargs),
stderr=subprocess.STDOUT)
def run_output(command, **kwargs):
return subprocess.check_output(
fmt_command(command, **kwargs),
stderr=subprocess.STDOUT)
def enqueue_job(event):
event_id = str(event['id'])
if (os.path.exists(os.path.join(args.project, event_id + '.ts')) or os.path.exists(os.path.join(args.project, event_id + '.mkv'))) and not args.force:
event_print(event, "file exist, skipping " + str(event['id']))
return
work_doc = os.path.join(tempdir.name, event_id + '.py')
work_comp = os.path.join(args.project, event_id + '.blend')
work_source = os.path.join(args.project, 'intro.blend')
intermediate_clip = os.path.join(tempdir.name, event_id + '.mkv')
if event_id == 'pause' or event_id == 'outro' or event_id == 'bgloop':
copyfile(args.project + event_id + '.blend', work_doc)
if platform.system() == 'Darwin':
run(r'/Applications/Blender.app/Contents/MacOS/Blender --background {comp} --use-extension 1 --threads 0 --render-output {locationpath} --render-anim',
comp=work_comp,
locationpath=intermediate_clip)
if platform.system() == 'Windows':
run(r'C:/Program\ Files/Blender\ Foundation/Blender\ 2.92/blender.exe --background {comp} --use-extension 1 --threads 0 --render-output {locationpath} --render-anim',
comp=work_comp,
locationpath=intermediate_clip)
if platform.system() == 'Linux':
run(r'blender --background {comp} --use-extension 1 --threads 0 --render-output {locationpath} --render-anim',
comp=work_comp,
locationpath=intermediate_clip)
else:
with open(args.project + 'intro.py', 'r') as fp:
scriptstr = fp.read()
for key, value in event.items():
value = str(value).replace('"', '\\"')
scriptstr = scriptstr.replace("$" + str(key), value)
with open(work_doc, 'w', encoding='utf-8') as fp:
fp.write(scriptstr)
if platform.system() == 'Darwin':
if args.debug:
print("running: Blender.app --background %s --python-use-system-env --python %s --use-extension 0 --threads 0 --render-output %s --render-anim" % (work_source, work_doc, intermediate_clip))
run(r'/Applications/Blender.app/Contents/MacOS/Blender --background {source} --python-use-system-env --python {jobpath} --use-extension 0 --threads 0 --render-output {locationpath} --render-anim',
source=work_source,
jobpath=work_doc,
locationpath=intermediate_clip)
if platform.system() == 'Windows':
if args.debug:
print("running: blender.exe --background %s --python-use-system-env --python %s --use-extension 0 --threads 0 --render-output %s --render-anim" % (work_source, work_doc, intermediate_clip))
run(r'C:/Program\ Files/Blender\ Foundation/Blender\ 2.92/blender.exe --background {source} --python-use-system-env --python {jobpath} --use-extension 0 --threads 0 --render-output {locationpath} --render-anim',
source=work_source,
jobpath=work_doc,
locationpath=intermediate_clip)
if platform.system() == 'Linux':
if args.debug:
print("running: blender --background %s --python-use-system-env --python %s --use-extension 0 --threads 0 --render-output %s --render-anim" % (work_source, work_doc, intermediate_clip))
run(r'blender --background {source} --python-use-system-env --python {jobpath} --use-extension 0 --threads 0 --render-output {locationpath} --render-anim',
source=work_source,
jobpath=work_doc,
locationpath=intermediate_clip)
if args.debug or args.keep:
copyfile(work_doc, args.project + event_id + '.py')
return event_id
def finalize_job(job_id, event):
event_id = str(event['id'])
intermediate_clip = os.path.join(tempdir.name, event_id + '.mkv')
final_clip = os.path.join(os.path.dirname(args.project), event_id + '.ts')
if args.alpha:
ffprobe = run_output('ffprobe -i {input} -show_streams -select_streams a -loglevel error',
input=intermediate_clip)
if ffprobe:
run('ffmpeg -threads 0 -y -hide_banner -loglevel error -i {input} -c:v qtrle -movflags faststart -aspect 16:9 -c:a mp2 -b:a 384k -shortest -f mov {output}',
input=intermediate_clip,
output=final_clip)
else:
run('ffmpeg -threads 0 -y -hide_banner -loglevel error -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=48000 -i {input} -c:v qtrle -movflags faststart -aspect 16:9 -c:a mp2 -b:a 384k -shortest -f mov {output}',
input=intermediate_clip,
output=final_clip)
else:
ffprobe = run_output('ffprobe -i {input} -show_streams -select_streams a -loglevel error',
input=intermediate_clip)
if ffprobe:
event_print(event, "finalize with audio from source file")
run('ffmpeg -threads 0 -y -hide_banner -loglevel error -i {input} -c:v mpeg2video -q:v 2 -aspect 16:9 -c:a mp2 -b:a 384k -shortest -f mpegts {output}',
input=intermediate_clip,
output=final_clip)
else:
event_print(event, "finalize with silent audio")
run('ffmpeg -threads 0 -y -hide_banner -loglevel error -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=48000 -i {input} -c:v mpeg2video -q:v 2 -aspect 16:9 -c:a mp2 -b:a 384k -shortest -f mpegts {output}',
input=intermediate_clip,
output=final_clip)
if event_id == 'pause' or event_id == 'outro' or event_id == 'bgloop':
event_print(event, "finalized " + str(event_id) + " to " + final_clip)
else:
event_print(event, "finalized intro to " + final_clip)
if args.ids:
if len(args.ids) == 1:
print("enqueuing {} job into blender".format(len(args.ids)))
else:
print("enqueuing {} jobs into blender".format(len(args.ids)))
else:
if len(events) == 1:
print("enqueuing {} job into blender".format(len(events)))
else:
print("enqueuing {} jobs into blender".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
if args.days and event['day'] not in args.days:
print("skipping day %s (%s)" % (event['day'], event['title']))
continue
for replace_id, replace_title in titlemap.items():
if str(event['id']) == str(replace_id):
event_print(event, "titlemap replacement")
event['title'] = str(replace_title['title'])
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
if not args.nof:
event_print(event, "finalizing job")
finalize_job(job_id, event)
else:
event_id = str(event['id'])
event_print(event, "skipping finalizing job")
if platform.system() == 'Windows':
intermediate_clip = os.path.join(tempdir.name, event_id + '.avi')
final_clip = os.path.join(os.path.dirname(args.project), event_id + '.avi')
else:
intermediate_clip = os.path.join(tempdir.name, event_id + '.mkv')
final_clip = os.path.join(os.path.dirname(args.project), event_id + '.mkv')
copyfile(intermediate_clip, final_clip)
event_print(event, "copied intermediate clip to " + final_clip)
if args.debug or args.keep:
print('keeping source files in ' + args.project)
else:
print('all done, cleaning up ' + tempdir.name)
tempdir.cleanup()