diff --git a/render.py b/render.py index 16a8ac7..16c0219 100644 --- a/render.py +++ b/render.py @@ -11,64 +11,14 @@ import time from pathlib import Path import ox -import lxml.etree from .pi import random -from .render_kdenlive import KDEnliveProject, _CACHE, melt_xml, get_melt +from .render_kdenlive import KDEnliveProject, _CACHE, get_melt +from .utils import resolve_roman, write_if_new, format_duration +from .render_utils import * default_prefix = "/srv/p_for_power" -def random_int(seq, length): - n = n_ = length - 1 - #print('len', n) - if n == 0: - return n - r = seq() / 9 * 10 - base = 10 - while n > 10: - n /= 10 - r += seq() / 9 * 10 - base += 10 - r = int(round(n_ * r / base)) - return r - -def random_choice(seq, items, pop=False): - n = random_int(seq, len(items)) - if pop: - return items.pop(n) - return items[n] - -def chance(seq, chance): - return (seq() / 10) < chance - -def get_clip_by_seqid(clips, seqid): - selected = None - for i, clip in enumerate(clips): - if clip['seqid'] == seqid: - selected = i - break - if selected is not None: - return clips.pop(i) - return None - - -def write_if_new(path, data, mode=''): - read_mode = 'r' + mode - write_mode = 'w' + mode - if os.path.exists(path): - with open(path, read_mode) as fd: - old = fd.read() - else: - old = "" - is_new = data != old - if path.endswith(".kdenlive"): - is_new = re.sub(r'\{.{36}\}', '', data) != re.sub(r'\{.{36}\}', '', old) - if is_new: - with open(path, write_mode) as fd: - fd.write(data) - -def format_duration(duration, fps): - return float('%0.5f' % (round(duration * fps) / fps)) def compose(clips, target=150, base=1024, voice_over=None, options=None): if options is None: @@ -80,13 +30,6 @@ def compose(clips, target=150, base=1024, voice_over=None, options=None): 'V1': [], 'V2': [], }, - 'back': { - 'V1': [], - 'V2': [], - }, - 'audio-back': { - 'A1': [], - }, 'audio-center': { 'A1': [], }, @@ -251,9 +194,7 @@ def compose(clips, target=150, base=1024, voice_over=None, options=None): #if chance(seq, 0.5): if chance(seq, 0.8): transparency_front = transparency - transparency_back = 0 else: - transparency_back = random_choice(seq, [0.25, 0.5, 0.75, 1]) transparency_front = 0 transparency_original = seq() / 9 transparency_original = 1 @@ -265,13 +206,6 @@ def compose(clips, target=150, base=1024, voice_over=None, options=None): 'transparency': transparency_front } }) - scene['back']['V2'].append({ - 'duration': clip['duration'], - 'src': clip['background'], - "filter": { - 'transparency': transparency_back - } - }) else: scene['front']['V1'].append({ 'duration': clip['duration'], @@ -280,34 +214,7 @@ def compose(clips, target=150, base=1024, voice_over=None, options=None): 'transparency': 0, } }) - scene['back']['V2'].append({ - 'duration': clip['duration'], - 'src': clip['original'], - "filter": { - 'transparency': 0, - } - }) - scene['back']['V1'].append({ - 'duration': clip['duration'], - 'src': clip['original'], - "filter": { - 'transparency': transparency_original, - } - }) - # 50 % chance to blur original from 0 to 30 - if chance(seq, 0.5): - blur = seq() * 3 - if blur: - scene['back']['V1'][-1]['filter']['blur'] = blur - volume_back = '-8.2' - if options.get('stereo_downmix'): - volume_back = '-7.2' - scene['audio-back']['A1'].append({ - 'duration': clip['duration'], - 'src': clip['original'], - 'filter': {'volume': volume_back}, - }) # TBD: Foley cf_volume = '-2.5' scene['audio-front']['A2'].append({ @@ -343,35 +250,6 @@ def compose(clips, target=150, base=1024, voice_over=None, options=None): print(scene['audio-center']['A1'][-1]) return scene, used -def get_track_duration(scene, k, n): - duration = 0 - for key, value in scene.items(): - if key == k: - for name, clips in value.items(): - if name == n: - for clip in clips: - duration += int(clip['duration'] * 24) - return duration / 24 - -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(): - for clip in clips: - duration += int(clip['duration'] * 24) - return duration / 24 - -def get_offset_duration(prefix): - duration = 0 - for root, folders, files in os.walk(prefix): - for f in files: - if f == 'scene.json': - duration += get_scene_duration(scene) - return duration - def write_subtitles(data, folder, options): data = fix_overlaps(data) path = folder / "front.srt" @@ -440,12 +318,6 @@ def render(root, scene, prefix='', options=None): files.append(path) return files -def get_project_duration(file): - out = melt_xml(file) - chain = lxml.etree.fromstring(out).xpath('producer')[0] - duration = int(chain.attrib['out']) + 1 - return duration - def get_fragments(clips, voice_over, prefix): import itemlist.models import item.models @@ -488,8 +360,6 @@ def get_fragments(clips, voice_over, prefix): #if set(clip['tags']) & set(fragment['tags']) and not set(clip['tags']) & set(fragment['anti-tags']): key = 'original' original = clip['original'] - if 'original_censored' in clip: - original = clip['original_censored'] if original in originals: fragment['clips'].append(clip) fragment["voice_over"] = voice_over.get(str(fragment["id"]), {}) @@ -497,19 +367,6 @@ def get_fragments(clips, voice_over, prefix): fragments.sort(key=lambda f: ox.sort_string(f['name'])) return fragments -def parse_lang(lang): - if lang and "," in lang: - lang = lang.split(',') - if isinstance(lang, list): - tlang = lang[1:] - lang = lang[0] - else: - tlang = None - if lang == "en": - lang = None - return lang, tlang - - def render_all(options): options = load_defaults(options) prefix = options['prefix'] @@ -589,7 +446,6 @@ def render_all(options): if ext == '.wav': cmd += ['vn=1'] else: - #if not timeline.endswith("back.kdenlive"): cmd += ['an=1'] cmd += ['vcodec=libx264', 'x264opts=keyint=1', 'crf=15'] subprocess.call(cmd) @@ -633,7 +489,6 @@ def render_all(options): "-map", "[a]", "-c:a", "aac", fragment_prefix / "audio-5.1.mp4" ]) audio_front = "audio-5.1.mp4" - audio_back = "audio-back.wav" copy = '-c' if options["stereo_downmix"]: cmds.append([ @@ -642,13 +497,11 @@ def render_all(options): "-i", fragment_prefix / "audio-front.wav", "-i", fragment_prefix / "audio-center.wav", "-i", fragment_prefix / "audio-rear.wav", - "-i", fragment_prefix / audio_back, "-filter_complex", "amix=inputs=4:duration=longest:dropout_transition=0", '-ac', '2', fragment_prefix / "audio-stereo.wav" ]) audio_front = "audio-stereo.wav" - audio_back = "audio-stereo.wav" copy = '-c:v' cmds.append([ @@ -660,22 +513,12 @@ def render_all(options): "-movflags", "+faststart", fragment_prefix / "front-mixed.mp4", ]) - cmds.append([ - "ffmpeg", "-y", - "-nostats", "-loglevel", "error", - "-i", fragment_prefix / "back.mp4", - "-i", fragment_prefix / audio_back, - "-c:v", "copy", - "-movflags", "+faststart", - fragment_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-mixed.mp4", "front.mp4"), ): duration_a = ox.avinfo(str(fragment_prefix / a))['duration'] @@ -684,16 +527,14 @@ def render_all(options): print('!!', duration_a, fragment_prefix / a) print('!!', duration_b, fragment_prefix / b) sys.exit(-1) - shutil.move(fragment_prefix / "back-audio.mp4", fragment_prefix / "back.mp4") shutil.move(fragment_prefix / "front-mixed.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", - "audio-front.wav", "audio-back.wav", "back-audio.mp4", + "audio-front.wav", "fl.wav", "fr.wav", "fc.wav", "lfe.wav", "bl.wav", "br.wav", "audio-stereo.wav", ): @@ -706,8 +547,6 @@ def render_all(options): base_prefix = Path(base_prefix) for timeline in ( "front", - "back", - "audio-back", "audio-center", "audio-front", "audio-rear", @@ -762,40 +601,19 @@ def render_all(options): "-movflags", "+faststart", base_prefix / "front-mixed.mp4", ]) - cmds.append([ - "ffmpeg", "-y", - "-nostats", "-loglevel", "error", - "-i", base_prefix / "back.mp4", - "-i", base_prefix / "audio-back.wav", - "-c:v", "copy", - "-movflags", "+faststart", - 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-mixed.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-mixed.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", + "audio-front.wav", "fl.wav", "fr.wav", "fc.wav", "lfe.wav", "bl.wav", "br.wav", ): fn = base_prefix / fn @@ -847,7 +665,6 @@ def add_translations_dict(sub, langs): values[slang] = tvalue return values - def get_srt(sub, offset, lang, tlang): sdata = sub.json(keys=['in', 'out', 'value']) sdata['value'] = sdata['value'].replace('
', '
').replace('
\n', '\n').replace('
', '\n').strip() @@ -862,23 +679,6 @@ def get_srt(sub, offset, lang, tlang): sdata["out"] += offset return sdata -def fix_overlaps(data): - previous = None - for sub in data: - if previous is None: - previous = sub - else: - if sub['in'] < previous['out']: - previous['out'] = sub['in'] - 0.001 - previous = sub - return data - -def shift_clips(data, offset): - for clip in data: - clip['in'] += offset - clip['out'] += offset - return data - def scene_subtitles(scene, options): import item.models offset = 0 @@ -940,77 +740,18 @@ def update_subtitles(options): subs = scene_subtitles(scene, options) write_subtitles(subs, folder, options) -def ass_encode(subs, options): - if "lang" in options: - langs = options["lang"].split(',') - else: - langs = list(subs[0]["values"]) - #print('ass_encode', langs, options) - #print(subs) - - header = '''[Script Info] -ScriptType: v4.00+ -PlayResX: 1920 -PlayResY: 1080 -ScaledBorderAndShadow: yes -YCbCr Matrix: None - -[V4+ Styles] -Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding -''' - ass = header - offset = options.get("sub_margin", 10) - spacing = options.get("sub_spacing", 20) - height = 42 - styles = [] - for lang in reversed(langs): - if isinstance(options.get("font"), list) and lang in options["font"]: - font = options["font"][lang] - else: - font = 'SimHei' if lang in ('zh', 'jp') else 'Menlo' - if isinstance(options.get("font_size"), list) and lang in options["font_size"]: - size = options["font_size"][lang] - else: - size = 46 if font == 'SimHei' else 42 - - styles.append( - f'Style: {lang},{font},{size},&Hffffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,{offset},1' - ) - offset += size + spacing - ass += '\n'.join(reversed(styles)) + '\n' - events = [ - 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text' - ] - for sub in subs: - start = ox.format_timecode(sub['in']).rstrip('0') - stop = ox.format_timecode(sub['out']).rstrip('0') - for lang in reversed(langs): - value = sub['values'][lang] - event = f'Dialogue: 0,{start},{stop},{lang},,0,0,0,,{value}' - events.append(event) - ass += '\n\n[Events]\n' + '\n'.join(events) + '\n' - return ass - def update_m3u(render_prefix, exclude=[]): - files = ox.sorted_strings(glob(render_prefix + "*/*/back.mp4")) + files = ox.sorted_strings(glob(render_prefix + "*/*/front.mp4")) for ex in exclude: files = [f for f in files if not f.startswith(ex + "/")] - back_m3u = "\n".join(files) - back_m3u = back_m3u.replace(render_prefix, "") - front_m3u = back_m3u.replace("back.mp4", "front.mp4") + front_m3u = "\n".join(files) + front_m3u = front_m3u.replace(render_prefix, "") - back_m3u_f = render_prefix + "back.m3u" front_m3u_f = render_prefix + "front.m3u" - with open(back_m3u_f + "_", "w") as fd: - fd.write(back_m3u) with open(front_m3u_f + "_", "w") as fd: fd.write(front_m3u) shutil.move(front_m3u_f + "_", front_m3u_f) - cmd = ["scp", front_m3u_f, "front:" + front_m3u_f] - subprocess.check_call(cmd) - shutil.move(back_m3u_f + "_", back_m3u_f) - def render_infinity(options): prefix = options['prefix'] @@ -1048,13 +789,7 @@ def render_infinity(options): folder = render_prefix + folder print("remove", folder) shutil.rmtree(folder) - cmd = ["ssh", "front", "rm", "-rf", folder] - #print(cmd) - subprocess.check_call(cmd) render_all(state) - path = "%s%s/" % (render_prefix, state["offset"]) - cmd = ['rsync', '-a', path, "front:" + path] - subprocess.check_call(cmd) update_m3u(render_prefix) state["offset"] += 1 with open(state_f + "~", "w") as fd: @@ -1063,17 +798,6 @@ def render_infinity(options): def join_subtitles(base_prefix, options): - ''' - 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)) - ''' scenes = list(sorted(glob('%s/*/scene.json' % base_prefix))) data = [] position = 0 @@ -1085,17 +809,6 @@ def join_subtitles(base_prefix, options): position += get_scene_duration(scene) write_subtitles(data, base_prefix, options) -def resolve_roman(s): - extra = re.compile(r'^\d+(.*?)$').findall(s) - if extra: - extra = extra[0].lower() - new = { - 'i': '1', 'ii': '2', 'iii': '3', 'iv': '4', 'v': '5', - 'vi': '6', 'vii': 7, 'viii': '8', 'ix': '9', 'x': '10' - }.get(extra, extra) - return s.replace(extra, new) - return s - def generate_clips(options): import item.models import itemlist.models @@ -1103,11 +816,6 @@ def generate_clips(options): options = load_defaults(options) prefix = options['prefix'] lang, tlang = parse_lang(options["lang"]) - if options['censored']: - censored_list = itemlist.models.List.get(options["censored"]) - censored = list(censored_list.get_items( - censored_list.user - ).all().values_list('public_id', flat=True)) clips = [] for i in item.models.Item.objects.filter(sort__type='original'): original_target = "" @@ -1131,9 +839,6 @@ def generate_clips(options): os.symlink(source, target) if type_ == "original": original_target = target - if options['censored'] and e.public_id in censored: - clip[type_ + "_censored"] = target - target = '/srv/t_for_time/censored.mp4' clip[type_] = target durations.append(e.files.filter(selected=True)[0].duration) clip["duration"] = min(durations) @@ -1141,11 +846,8 @@ def generate_clips(options): print('!!', durations, clip) continue cd = format_duration(clip["duration"], 24) - #if cd != clip["duration"]: - # print(clip["duration"], '->', cd, durations, clip) clip["duration"] = cd clip['tags'] = i.data.get('tags', []) - clip['editingtags'] = i.data.get('editingtags', []) name = os.path.basename(original_target) seqid = re.sub(r"Hotel Aporia_(\d+)", "S\\1_", name) seqid = re.sub(r"Night March_(\d+)", "S\\1_", seqid) @@ -1161,12 +863,8 @@ def generate_clips(options): except: print(name, seqid, 'failed') raise - if "original" in clip and "foreground" in clip and "background" in clip: - clips.append(clip) - elif "original" in clip and "animation" in clip: - clips.append(clip) - else: - print("ignoring incomplete video", i) + + clips.append(clip) with open(os.path.join(prefix, 'clips.json'), 'w') as fd: json.dump(clips, fd, indent=2, ensure_ascii=False) @@ -1199,18 +897,3 @@ def generate_clips(options): } with open(os.path.join(prefix, 'voice_over.json'), 'w') as fd: json.dump(voice_over, fd, indent=2, ensure_ascii=False) - - if options['censored']: - censored_mp4 = '/srv/t_for_time/censored.mp4' - if not os.path.exists(censored_mp4): - cmd = [ - "ffmpeg", - "-nostats", "-loglevel", "error", - "-f", "lavfi", - "-i", "color=color=white:size=1920x1080:rate=24", - "-t", "3600", - "-c:v", "libx264", - "-pix_fmt", "yuv420p", - censored_mp4 - ] - subprocess.call(cmd) diff --git a/render_utils.py b/render_utils.py new file mode 100644 index 0000000..424d0c1 --- /dev/null +++ b/render_utils.py @@ -0,0 +1,159 @@ +import re +import os + +import lxml.etree + +from .render_kdenlive melt_xml, get_melt + + +def parse_lang(lang): + if lang and "," in lang: + lang = lang.split(',') + if isinstance(lang, list): + tlang = lang[1:] + lang = lang[0] + else: + tlang = None + if lang == "en": + lang = None + return lang, tlang + +def random_int(seq, length): + n = n_ = length - 1 + #print('len', n) + if n == 0: + return n + r = seq() / 9 * 10 + base = 10 + while n > 10: + n /= 10 + r += seq() / 9 * 10 + base += 10 + r = int(round(n_ * r / base)) + return r + +def random_choice(seq, items, pop=False): + n = random_int(seq, len(items)) + if pop: + return items.pop(n) + return items[n] + +def chance(seq, chance): + return (seq() / 10) < chance + +def get_clip_by_seqid(clips, seqid): + selected = None + for i, clip in enumerate(clips): + if clip['seqid'] == seqid: + selected = i + break + if selected is not None: + return clips.pop(i) + return None + + +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(): + for clip in clips: + duration += int(clip['duration'] * 24) + return duration / 24 + + +def get_offset_duration(prefix): + duration = 0 + for root, folders, files in os.walk(prefix): + for f in files: + if f == 'scene.json': + duration += get_scene_duration(scene) + return duration + + +def get_track_duration(scene, k, n): + duration = 0 + for key, value in scene.items(): + if key == k: + for name, clips in value.items(): + if name == n: + for clip in clips: + duration += int(clip['duration'] * 24) + return duration / 24 + +def get_project_duration(file): + out = melt_xml(file) + chain = lxml.etree.fromstring(out).xpath('producer')[0] + duration = int(chain.attrib['out']) + 1 + return duration + +def fix_overlaps(data): + previous = None + for sub in data: + if previous is None: + previous = sub + else: + if sub['in'] < previous['out']: + previous['out'] = sub['in'] - 0.001 + previous = sub + return data + +def shift_clips(data, offset): + for clip in data: + clip['in'] += offset + clip['out'] += offset + return data + +def ass_encode(subs, options): + if "lang" in options: + langs = options["lang"].split(',') + else: + langs = list(subs[0]["values"]) + #print('ass_encode', langs, options) + #print(subs) + + header = '''[Script Info] +ScriptType: v4.00+ +PlayResX: 1920 +PlayResY: 1080 +ScaledBorderAndShadow: yes +YCbCr Matrix: None + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +''' + ass = header + offset = options.get("sub_margin", 10) + spacing = options.get("sub_spacing", 20) + height = 42 + styles = [] + for lang in reversed(langs): + if isinstance(options.get("font"), list) and lang in options["font"]: + font = options["font"][lang] + else: + font = 'SimHei' if lang in ('zh', 'jp') else 'Menlo' + if isinstance(options.get("font_size"), list) and lang in options["font_size"]: + size = options["font_size"][lang] + else: + size = 46 if font == 'SimHei' else 42 + + styles.append( + f'Style: {lang},{font},{size},&Hffffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,{offset},1' + ) + offset += size + spacing + ass += '\n'.join(reversed(styles)) + '\n' + events = [ + 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text' + ] + for sub in subs: + start = ox.format_timecode(sub['in']).rstrip('0') + stop = ox.format_timecode(sub['out']).rstrip('0') + for lang in reversed(langs): + value = sub['values'][lang] + event = f'Dialogue: 0,{start},{stop},{lang},,0,0,0,,{value}' + events.append(event) + ass += '\n\n[Events]\n' + '\n'.join(events) + '\n' + return ass + diff --git a/utils.py b/utils.py index b6efc6d..8824d2b 100644 --- a/utils.py +++ b/utils.py @@ -1,4 +1,16 @@ +import os +import re +def resolve_roman(s): + extra = re.compile(r'^\d+(.*?)$').findall(s) + if extra: + extra = extra[0].lower() + new = { + 'i': '1', 'ii': '2', 'iii': '3', 'iv': '4', 'v': '5', + 'vi': '6', 'vii': 7, 'viii': '8', 'ix': '9', 'x': '10' + }.get(extra, extra) + return s.replace(extra, new) + return s def upgrade_originals(): import item.models @@ -30,3 +42,21 @@ def remove_deselected_files(): if changed: i.save() +def write_if_new(path, data, mode=''): + read_mode = 'r' + mode + write_mode = 'w' + mode + if os.path.exists(path): + with open(path, read_mode) as fd: + old = fd.read() + else: + old = "" + is_new = data != old + if path.endswith(".kdenlive"): + is_new = re.sub(r'\{.{36}\}', '', data) != re.sub(r'\{.{36}\}', '', old) + if is_new: + with open(path, write_mode) as fd: + fd.write(data) + +def format_duration(duration, fps): + return float('%0.5f' % (round(duration * fps) / fps)) +