#!/usr/bin/env python3 # vim: tabstop=4 shiftwidth=4 expandtab import subprocess import schedulelib import argparse import tempfile import shlex import time import sys import os import platform from shutil import copyfile titlemap = ( {'id': "000000", 'title': "Short title goes here"}, ) # Parse arguments parser = argparse.ArgumentParser( description='C3VOC Intro-Outro-Generator - Variant to use with Adobe After Effects Files', usage="./make-adobe-after-effects.py yourproject/ https://url/to/schedule.xml filename_of_intro.aepx", formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('project', action="store", metavar='Project folder', type=str, help=''' Path to your project folder with After Effects Files ''') parser.add_argument('schedule', action="store", metavar='Schedule-URL', type=str, nargs='?', help=''' URL or Path to your schedule.xml ''') parser.add_argument('introfile', action="store", metavar='Name of intro source file', default='intro.aepx', type=str, nargs='?', help=''' Filename of the intro source file inside the 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. Schedule-URL can be left blank when used with --debug This argument must not be used together with --id Usage: ./make-adobe-after-effects.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('--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-adobe-after-effects.py yourproject/ --day "1" "3" ''') parser.add_argument('--pause', action="store_true", default=False, help=''' Render a pause loop from the pause.aepx 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.aepx file in the project folder. ''') parser.add_argument('--bgloop', action="store_true", default=False, help=''' Render background loop from the bgloop.aepx file in the project folder. ''') parser.add_argument('--keep', action="store_true", default=False, help=''' Keep source file in the project folder after render. ''') parser.add_argument('--mp4', action="store_true", default=False, help=''' Also create a MP4 for preview. ''') 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 After Effect 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(schedulelib.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_output(command, **kwargs): return subprocess.check_output( fmt_command(command, **kwargs), stderr=subprocess.STDOUT) def run_once(command, **kwargs): DETACHED_PROCESS = 0x00000008 return subprocess.Popen( fmt_command(command, **kwargs), shell=False, stdin=None, stdout=None, stderr=None, close_fds=True, creationflags=DETACHED_PROCESS) 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 + '.mov'))) and not args.force: event_print(event, "file exist, skipping " + str(event['id'])) return work_doc = os.path.join(tempdir.name, event_id + '.aepx') script_doc = os.path.join(tempdir.name, event_id+'.jsx') ascript_doc = os.path.join(tempdir.name, event_id+'.scpt') intermediate_clip = os.path.join(tempdir.name, event_id + '.mov') if event_id == 'pause' or event_id == 'outro' or event_id == 'bgloop': copyfile(args.project + event_id + '.aepx', work_doc) if platform.system() == 'Darwin': run(r'/Applications/Adobe\ After\ Effects\ 2024/aerender -project {jobpath} -comp {comp} -mp -output {locationpath}', jobpath=work_doc, comp=event_id, locationpath=intermediate_clip) if platform.system() == 'Windows': run(r'C:/Program\ Files/Adobe/Adobe\ After\ Effects\ 2024/Support\ Files/aerender.exe -project {jobpath} -comp {comp} -mp -output {locationpath}', jobpath=work_doc, comp=event_id, locationpath=intermediate_clip) else: with open(args.project + 'intro.jsx', 'r') as fp: scriptstr = fp.read() scriptstr = scriptstr.replace("$filename", work_doc.replace("\\", "/")) for key, value in event.items(): value = str(value).replace('"', '\\"') scriptstr = scriptstr.replace("$" + str(key), value) with open(script_doc, 'w', encoding='utf-8') as fp: fp.write(scriptstr) copyfile(args.project+args.introfile, work_doc) if platform.system() == 'Darwin': copyfile(args.project+'intro.scpt', ascript_doc) # run('osascript {ascript_path} {scriptpath}', # scriptpath=script_doc, # ascript_path=ascript_doc) run('osascript {ascript_path} {jobpath} {scriptpath}', jobpath=work_doc, scriptpath=script_doc, ascript_path=ascript_doc) run(r'/Applications/Adobe\ After\ Effects\ 2024/aerender -project {jobpath} -comp "intro" -mp -output {locationpath}', jobpath=work_doc, locationpath=intermediate_clip) if platform.system() == 'Windows': run_once(r'C:/Program\ Files/Adobe/Adobe\ After\ Effects\ 2024/Support\ Files/AfterFX.exe -noui -r {scriptpath}', scriptpath=script_doc) time.sleep(5) run(r'C:/Program\ Files/Adobe/Adobe\ After\ Effects\ 2024/Support\ Files/aerender.exe -project {jobpath} -comp "intro" -mfr on 100 -output {locationpath}', jobpath=work_doc, locationpath=intermediate_clip) if args.debug or args.keep: path = tempdir.name dirs = os.listdir(path) for file in dirs: print(file) copyfile(work_doc, args.project + event_id + '.aepx') copyfile(script_doc, args.project + event_id + '.jsx') copyfile(intermediate_clip, args.project + event_id + '.mov') return event_id def finalize_job(job_id, event): event_id = str(event['id']) intermediate_clip = os.path.join(tempdir.name, event_id + '.mov') final_clip = os.path.join(os.path.dirname(args.project), event_id + '.ts') preview = os.path.join(os.path.dirname(args.project), event_id + '.mp4') 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.mp4: run('ffmpeg -threads 0 -y -hide_banner -loglevel error -i {input} {output}', input=final_clip, output=preview) event_print(event, "created mp4 preview " + preview) if args.ids: if len(args.ids) == 1: print("enqueuing {} job into aerender".format(len(args.ids))) else: print("enqueuing {} jobs into aerender".format(len(args.ids))) else: if len(events) == 1: print("enqueuing {} job into aerender".format(len(events))) else: print("enqueuing {} jobs into aerender".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 item in titlemap: if str(item['id']) == str(event['id']): title = item['title'] event_print(event, "titlemap replacement") event_print(event, "replacing title %s with %s" % (event['title'], title)) event['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 + '.mov') final_clip = os.path.join(os.path.dirname(args.project), event_id + '.mov') 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()