502 lines
14 KiB
Python
Executable file
502 lines
14 KiB
Python
Executable file
#!/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(dir=os.getcwd())
|
||
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()
|