diff --git a/management/commands/render.py b/management/commands/render.py index ace0d61..54e66c2 100644 --- a/management/commands/render.py +++ b/management/commands/render.py @@ -16,6 +16,9 @@ class Command(BaseCommand): parser.add_argument('--duration', action='store', dest='duration', default="3600", help='target duration of all fragments in seconds') parser.add_argument('--offset', action='store', dest='offset', default="1024", help='inital offset in pi') parser.add_argument('--no-video', action='store_true', dest='no_video', default=False, help='don\'t render video') + parser.add_argument('--single-file', action='store_true', dest='single_file', default=False, help='render to single video') + parser.add_argument('--keep-audio', action='store_true', dest='keep_audio', default=False, help='keep independent audio tracks') + parser.add_argument('--debug', action='store_true', dest='debug', default=False, help='output more info') def handle(self, **options): render_all(options) diff --git a/render.py b/render.py index 97d412a..32d43a7 100644 --- a/render.py +++ b/render.py @@ -313,6 +313,9 @@ def compose(clips, target=150, base=1024, voice_over=None): return scene, used def get_scene_duration(scene): + if isinstance(scene, str): + with open(scene) as fd: + scene = json.load(fd) duration = 0 for key, value in scene.items(): for name, clips in value.items(): @@ -325,8 +328,6 @@ def get_offset_duration(prefix): for root, folders, files in os.walk(prefix): for f in files: if f == 'scene.json': - path = os.path.join(root, f) - scene = json.load(open(path)) duration += get_scene_duration(scene) return duration @@ -414,6 +415,8 @@ def get_fragments(clips, voice_over, prefix): return fragments +def render_timeline(options): + def render_all(options): prefix = options['prefix'] duration = int(options['duration']) @@ -472,7 +475,7 @@ def render_all(options): scene_json = json.dumps(scene, indent=2, ensure_ascii=False) write_if_new(os.path.join(fragment_prefix, 'scene.json'), scene_json) - if not options['no_video']: + if not options['no_video'] and not options["single_file"]: for timeline in timelines: print(timeline) ext = '.mp4' @@ -502,8 +505,8 @@ def render_all(options): subprocess.call(cmd) os.unlink(timeline.replace('.kdenlive', ext)) - fragment_prefix = Path(fragment_prefix) cmds = [] + fragment_prefix = Path(fragment_prefix) for src, out1, out2 in ( ("audio-front.wav", "fl.wav", "fr.wav"), ("audio-center.wav", "fc.wav", "lfe.wav"), @@ -547,7 +550,8 @@ def render_all(options): fragment_prefix / "back-audio.mp4", ]) for cmd in cmds: - #print(" ".join([str(x) for x in cmd])) + if options["debug"]: + print(" ".join([str(x) for x in cmd])) subprocess.call(cmd) for a, b in ( @@ -562,6 +566,10 @@ def render_all(options): sys.exit(-1) shutil.move(fragment_prefix / "back-audio.mp4", fragment_prefix / "back.mp4") shutil.move(fragment_prefix / "front-5.1.mp4", fragment_prefix / "front.mp4") + if options["keep_audio"]: + shutil.move(fragment_prefix / "audio-center.wav", fragment_prefix / "vocals.wav") + shutil.move(fragment_prefix / "audio-front.wav", fragment_prefix / "foley.wav") + shutil.move(fragment_prefix / "audio-back.wav", fragment_prefix / "original.wav") for fn in ( "audio-5.1.mp4", "audio-center.wav", "audio-rear.wav", @@ -572,6 +580,109 @@ def render_all(options): if os.path.exists(fn): os.unlink(fn) + if options["single_file"]: + cmds = [] + base_prefix = Path(base_prefix) + for timeline in ( + "front", + "back", + "audio-back", + "audio-center", + "audio-front", + "audio-rear", + ): + timelines = list(sorted(glob('%s/*/%s.kdenlive' % (base_prefix, timeline)))) + ext = '.mp4' + if '/audio' in timelines[0]: + ext = '.wav' + out = base_prefix / (timeline + ext) + cmd = [ + 'xvfb-run', '-a', + 'melt' + ] + timelines + [ + '-quiet', + '-consumer', 'avformat:%s' % out, + ] + if ext == '.wav': + cmd += ['vn=1'] + else: + cmd += ['an=1'] + cmd += ['vcodec=libx264', 'x264opts=keyint=1', 'crf=15'] + cmds.append(cmd) + for src, out1, out2 in ( + ("audio-front.wav", "fl.wav", "fr.wav"), + ("audio-center.wav", "fc.wav", "lfe.wav"), + ("audio-rear.wav", "bl.wav", "br.wav"), + ): + cmds.append([ + "ffmpeg", "-y", + "-nostats", "-loglevel", "error", + "-i", base_prefix / src, + "-filter_complex", + "[0:0]pan=1|c0=c0[left]; [0:0]pan=1|c0=c1[right]", + "-map", "[left]", base_prefix / out1, + "-map", "[right]", base_prefix / out2, + ]) + cmds.append([ + "ffmpeg", "-y", + "-nostats", "-loglevel", "error", + "-i", base_prefix / "fl.wav", + "-i", base_prefix / "fr.wav", + "-i", base_prefix / "fc.wav", + "-i", base_prefix / "lfe.wav", + "-i", base_prefix / "bl.wav", + "-i", base_prefix / "br.wav", + "-filter_complex", "[0:a][1:a][2:a][3:a][4:a][5:a]amerge=inputs=6[a]", + "-map", "[a]", "-c:a", "aac", base_prefix / "audio-5.1.mp4" + ]) + cmds.append([ + "ffmpeg", "-y", + "-nostats", "-loglevel", "error", + "-i", base_prefix / "front.mp4", + "-i", base_prefix / "audio-5.1.mp4", + "-c", "copy", + base_prefix / "front-5.1.mp4", + ]) + cmds.append([ + "ffmpeg", "-y", + "-nostats", "-loglevel", "error", + "-i", base_prefix / "back.mp4", + "-i", base_prefix / "audio-back.wav", + "-c:v", "copy", + base_prefix / "back-audio.mp4", + ]) + for cmd in cmds: + if options["debug"]: + print(" ".join([str(x) for x in cmd])) + subprocess.call(cmd) + + for a, b in ( + ("back-audio.mp4", "back.mp4"), + ("front-5.1.mp4", "back.mp4"), + ): + duration_a = ox.avinfo(str(base_prefix / a))['duration'] + duration_b = ox.avinfo(str(base_prefix / b))['duration'] + if duration_a != duration_b: + print('!!', duration_a, base_prefix / a) + print('!!', duration_b, base_prefix / b) + sys.exit(-1) + shutil.move(base_prefix / "back-audio.mp4", base_prefix / "back.mp4") + shutil.move(base_prefix / "front-5.1.mp4", base_prefix / "front.mp4") + if options["keep_audio"]: + shutil.move(base_prefix / "audio-center.wav", base_prefix / "vocals.wav") + shutil.move(base_prefix / "audio-front.wav", base_prefix / "foley.wav") + shutil.move(base_prefix / "audio-back.wav", base_prefix / "original.wav") + for fn in ( + "audio-5.1.mp4", + "audio-center.wav", "audio-rear.wav", + "audio-front.wav", "audio-back.wav", "back-audio.mp4", + "fl.wav", "fr.wav", "fc.wav", "lfe.wav", "bl.wav", "br.wav", + ): + fn = base_prefix / fn + if os.path.exists(fn): + os.unlink(fn) + join_subtitles(base_prefix) + print("Duration - Target: %s Actual: %s" % (target_position, position)) print(json.dumps(dict(stats), sort_keys=True, indent=2)) with open(_cache, "w") as fd: @@ -727,3 +838,15 @@ def render_infinity(options): with open(state_f + "~", "w") as fd: json.dump(state, fd, indent=2) shutil.move(state_f + "~", state_f) + + +def join_subtitles(base_prefix): + subtitles = list(sorted(glob('%s/*/front.srt' % base_prefix))) + data = [] + position = 0 + for srt in subtitles: + scene = srt.replace('front.srt', 'scene.json') + data += ox.srt.load(srt, offset=position) + position += get_scene_duration(scene) + with open(base_prefix / 'front.srt', 'wb') as fd: + fd.write(ox.srt.encode(data))